tksbrokerapi.TKSBrokerAPI
TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios,
as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
from the console, it has a rich keys and commands, or you can use it as Python module with python import.
TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
- Open account for trading: http://tinkoff.ru/sl/AaX1Et1omnH
- TKSBrokerAPI module documentation: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
- See CLI examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
- Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
- About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
- Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
1# -*- coding: utf-8 -*- 2# Author: Timur Gilmullin 3 4""" 5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios, 6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: 7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`. 8 9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive 10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems. 11 12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH 13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html 14- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples 15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html 16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/ 17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/ 18""" 19 20# Copyright (c) 2022 Gilmillin Timur Mansurovich 21# 22# Licensed under the Apache License, Version 2.0 (the "License"); 23# you may not use this file except in compliance with the License. 24# You may obtain a copy of the License at 25# 26# http://www.apache.org/licenses/LICENSE-2.0 27# 28# Unless required by applicable law or agreed to in writing, software 29# distributed under the License is distributed on an "AS IS" BASIS, 30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31# See the License for the specific language governing permissions and 32# limitations under the License. 33 34 35import sys 36import os 37from argparse import ArgumentParser 38from importlib.metadata import version 39 40from datetime import datetime, timedelta 41from dateutil.tz import tzlocal, tzutc 42from time import sleep 43 44import re 45import json 46import requests 47import traceback as tb 48from typing import Union 49 50from multiprocessing import cpu_count 51from multiprocessing.pool import ThreadPool 52import pandas as pd 53 54from TKSEnums import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 55from TradeRoutines import * # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module 56 57from pricegenerator.PriceGenerator import PriceGenerator, uLogger # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator 58from pricegenerator.UniLogger import DisableLogger as PGDisLog # Method for disable log from PriceGenerator 59 60import UniLogger as uLog # Logger for TKSBrokerAPI 61 62 63# --- Common technical parameters: 64 65PGDisLog(uLogger.handlers[0]) # Disable 3-rd party logging from PriceGenerator 66uLogger = uLog.UniLogger # init logger for TKSBrokerAPI 67uLogger.level = 10 # debug level by default for TKSBrokerAPI module 68uLogger.handlers[0].level = 20 # info level by default for STDOUT of TKSBrokerAPI module 69 70__version__ = "1.5" # The "major.minor" version setup here, but build number define at the build-server only 71 72CPU_COUNT = cpu_count() # host's real CPU count 73CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1 # how many CPUs will be used for parallel calculations 74 75 76class TinkoffBrokerServer: 77 """ 78 This class implements methods to work with Tinkoff broker server. 79 80 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 81 82 About `token`: https://tinkoff.github.io/investAPI/token/ 83 """ 84 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 85 """ 86 Main class init. 87 88 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 89 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 90 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 91 :param useCache: use default cache file with raw data to use instead of `iList`. 92 True by default. Cache is auto-update if new day has come. 93 If you don't want to use cache and always updates raw data then set `useCache=False`. 94 :param defaultCache: path to default cache file. `dump.json` by default. 95 """ 96 if token is None or not token: 97 try: 98 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 99 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 100 101 except KeyError: 102 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 103 raise Exception("Token required") 104 105 else: 106 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 107 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 108 109 if accountId is None or not accountId: 110 try: 111 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 112 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 113 114 except KeyError: 115 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 116 117 else: 118 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 119 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 120 121 self.version = __version__ # duplicate here used TKSBrokerAPI main version 122 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 123 124 Latest version: https://pypi.org/project/tksbrokerapi/ 125 """ 126 127 self.aliases = TKS_TICKER_ALIASES 128 """Some aliases instead official tickers. 129 130 See also: `TKSEnums.TKS_TICKER_ALIASES` 131 """ 132 133 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 134 135 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 136 137 self.ticker = "" 138 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 139 140 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 141 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 142 143 See also: `SearchByTicker()`, `SearchInstruments()`. 144 """ 145 146 self.figi = "" 147 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 148 149 See also: `SearchByFIGI()`, `SearchInstruments()`. 150 """ 151 152 self.depth = 1 153 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 154 155 See also: `GetCurrentPrices()`. 156 """ 157 158 self.server = r"https://invest-public-api.tinkoff.ru/rest" 159 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 160 161 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 162 """ 163 164 uLogger.debug("Broker API server: {}".format(self.server)) 165 166 self.timeout = 15 167 """Server operations timeout in seconds. Default: `15`. 168 169 See also: `SendAPIRequest()`. 170 """ 171 172 self.headers = { 173 "Content-Type": "application/json", 174 "accept": "application/json", 175 "Authorization": "Bearer {}".format(self.token), 176 "x-app-name": "Tim55667757.TKSBrokerAPI", 177 } 178 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 179 180 See also: `SendAPIRequest()`. 181 """ 182 183 self.body = None 184 """Request body which send to broker server. Default: `None`. 185 186 See also: `SendAPIRequest()`. 187 """ 188 189 self.moreDebug = False 190 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 191 192 self.historyFile = None 193 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 194 195 See also: `History()`. 196 """ 197 198 self.htmlHistoryFile = "index.html" 199 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 200 201 See also: `ShowHistoryChart()`. 202 """ 203 204 self.instrumentsFile = "instruments.md" 205 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 206 207 See also: `ShowInstrumentsInfo()`. 208 """ 209 210 self.searchResultsFile = "search-results.md" 211 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 212 213 See also: `SearchInstruments()`. 214 """ 215 216 self.pricesFile = "prices.md" 217 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 218 219 See also: `GetListOfPrices()`. 220 """ 221 222 self.infoFile = "info.md" 223 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 224 225 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 226 """ 227 228 self.bondsXLSXFile = "ext-bonds.xlsx" 229 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 230 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 231 232 See also: `ExtendBondsData()`. 233 """ 234 235 self.calendarFile = "calendar.md" 236 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 237 238 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 239 240 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 241 """ 242 243 self.overviewFile = "overview.md" 244 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 245 246 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 247 """ 248 249 self.overviewDigestFile = "overview-digest.md" 250 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 251 252 See also: `Overview()` with parameter `details="digest"`. 253 """ 254 255 self.overviewPositionsFile = "overview-positions.md" 256 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 257 258 See also: `Overview()` with parameter `details="positions"`. 259 """ 260 261 self.overviewOrdersFile = "overview-orders.md" 262 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 263 264 See also: `Overview()` with parameter `details="orders"`. 265 """ 266 267 self.overviewAnalyticsFile = "overview-analytics.md" 268 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 269 270 See also: `Overview()` with parameter `details="analytics"`. 271 """ 272 273 self.overviewBondsCalendarFile = "overview-calendar.md" 274 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 275 276 See also: `Overview()` with parameter `details="calendar"`. 277 """ 278 279 self.reportFile = "deals.md" 280 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 281 282 See also: `Deals()`. 283 """ 284 285 self.withdrawalLimitsFile = "limits.md" 286 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 287 288 See also: `OverviewLimits()` and `RequestLimits()`. 289 """ 290 291 self.userInfoFile = "user-info.md" 292 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 293 294 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 295 """ 296 297 self.userAccountsFile = "accounts.md" 298 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 299 300 See also: `OverviewAccounts()`, `RequestAccounts()`. 301 """ 302 303 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 304 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 305 306 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 307 308 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 309 """ 310 311 self.iList = None # init iList for raw instruments data 312 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 313 314 See also: `Listing()`, `DumpInstruments()`. 315 """ 316 317 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 318 if useCache: 319 if os.path.exists(self.iListDumpFile): 320 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 321 curTime = datetime.now(tzutc()) 322 323 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 324 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 325 326 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 327 328 else: 329 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 330 331 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 332 os.path.abspath(self.iListDumpFile), 333 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 334 )) 335 336 else: 337 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 338 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 339 340 else: 341 self.iList = self.Listing() # request new raw instruments data from broker server 342 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 343 344 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 345 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 346 347 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 348 """ 349 350 def _ParseJSON(self, rawData="{}") -> dict: 351 """ 352 Parse JSON from response string. 353 354 :param rawData: this is a string with JSON-formatted text. 355 :return: JSON (dictionary), parsed from server response string. 356 """ 357 responseJSON = json.loads(rawData) if rawData else {} 358 359 if self.moreDebug: 360 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 361 362 return responseJSON 363 364 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 365 """ 366 Send GET or POST request to broker server and receive JSON object. 367 368 self.header: must be defining with dictionary of headers. 369 self.body: if define then used as request body. None by default. 370 self.timeout: global request timeout, 15 seconds by default. 371 :param url: url with REST request. 372 :param reqType: send "GET" or "POST" request. "GET" by default. 373 :param retry: how many times retry after first request if an 5xx server errors occurred. 374 :param pause: sleep time in seconds between retries. 375 :return: response JSON (dictionary) from broker. 376 """ 377 if reqType not in ("GET", "POST"): 378 uLogger.error("You can define request type: 'GET' or 'POST'!") 379 raise Exception("Incorrect value") 380 381 if self.moreDebug: 382 uLogger.debug("Request parameters:") 383 uLogger.debug(" - REST API URL: {}".format(url)) 384 uLogger.debug(" - request type: {}".format(reqType)) 385 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 386 uLogger.debug(" - body:\n{}".format(self.body)) 387 388 # fast hack to avoid all operations with some tickers/FIGI 389 responseJSON = {} 390 oK = True 391 for item in self.exclude: 392 if item in url: 393 if self.moreDebug: 394 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 395 396 oK = False 397 break 398 399 if oK: 400 counter = 0 401 response = None 402 errMsg = "" 403 404 while not response and counter <= retry: 405 if reqType == "GET": 406 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 407 408 if reqType == "POST": 409 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 410 411 if self.moreDebug: 412 uLogger.debug("Response:") 413 uLogger.debug(" - status code: {}".format(response.status_code)) 414 uLogger.debug(" - reason: {}".format(response.reason)) 415 uLogger.debug(" - body length: {}".format(len(response.text))) 416 uLogger.debug(" - headers:\n{}".format(response.headers)) 417 418 # Server returns some headers: 419 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 420 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 421 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 422 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 423 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 424 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 425 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 426 sleep(rateLimitWait) 427 428 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 429 if 400 <= response.status_code < 500: 430 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 431 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 432 counter = retry + 1 433 434 if 500 <= response.status_code < 600: 435 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 436 uLogger.debug(" - not oK, {}".format(errMsg)) 437 counter += 1 438 439 if counter <= retry: 440 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 441 sleep(pause) 442 443 responseJSON = self._ParseJSON(rawData=response.text) 444 445 if errMsg: 446 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 447 uLogger.error(" - not oK, {}".format(errMsg)) 448 449 return responseJSON 450 451 def _IUpdater(self, iType: str) -> tuple: 452 """ 453 Request instrument by type from server. See available API methods for instruments: 454 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 455 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 456 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 457 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 458 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 459 460 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 461 :return: tuple with iType name and list of available instruments of current type for defined user token. 462 """ 463 result = [] 464 465 if iType in TKS_INSTRUMENTS: 466 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 467 468 # all instruments have the same body in API v2 requests: 469 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 470 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 471 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 472 473 return iType, result 474 475 def _IWrapper(self, kwargs): 476 """ 477 Wrapper runs instrument's update method `_IUpdater()`. 478 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 479 """ 480 return self._IUpdater(**kwargs) 481 482 def Listing(self) -> dict: 483 """ 484 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 485 486 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 487 """ 488 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 489 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 490 491 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 492 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 493 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 494 495 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 496 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 497 poolUpdater.close() 498 499 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 500 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 501 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 502 503 # calculate minimum price increment (step) for all instruments and set up instrument's type: 504 for iType in iList.keys(): 505 for ticker in iList[iType]: 506 iList[iType][ticker]["type"] = iType 507 508 if "minPriceIncrement" in iList[iType][ticker].keys(): 509 iList[iType][ticker]["step"] = NanoToFloat( 510 iList[iType][ticker]["minPriceIncrement"]["units"], 511 iList[iType][ticker]["minPriceIncrement"]["nano"], 512 ) 513 514 else: 515 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 516 517 return iList 518 519 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 520 """ 521 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 522 523 See also: `DumpInstruments()`, `Listing()`. 524 525 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 526 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 527 """ 528 if self.iListDumpFile is None or not self.iListDumpFile: 529 uLogger.error("Output name of dump file must be defined!") 530 raise Exception("Filename required") 531 532 if not self.iList or forceUpdate: 533 self.iList = self.Listing() 534 535 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 536 537 # Save as XLSX with separated sheets for every type of instruments: 538 with pd.ExcelWriter( 539 path=xlsxDumpFile, 540 date_format=TKS_DATE_FORMAT, 541 datetime_format=TKS_DATE_TIME_FORMAT, 542 mode="w", 543 ) as writer: 544 for iType in TKS_INSTRUMENTS: 545 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 546 df = df[sorted(df)] # sorted by column names 547 df = df.applymap( 548 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 549 na_action="ignore", 550 ) # converting numbers from nano-type to float in every cell 551 df.to_excel( 552 writer, 553 sheet_name=iType, 554 encoding="UTF-8", 555 freeze_panes=(1, 1), 556 ) # saving as XLSX-file with freeze first row and column as headers 557 558 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 559 560 def DumpInstruments(self, forceUpdate: bool = True) -> str: 561 """ 562 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 563 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 564 565 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 566 567 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 568 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 569 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 570 """ 571 if self.iListDumpFile is None or not self.iListDumpFile: 572 uLogger.error("Output name of dump file must be defined!") 573 raise Exception("Filename required") 574 575 if not self.iList or forceUpdate: 576 self.iList = self.Listing() 577 578 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 579 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 580 fH.write(jsonDump) 581 582 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 583 584 return jsonDump 585 586 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 587 """ 588 Show information about one instrument defined by json data and prints it in Markdown format. 589 590 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 591 592 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 593 :param show: if `True` then also printing information about instrument and its current price. 594 :return: multilines text in Markdown format with information about one instrument. 595 """ 596 splitLine = "| | |\n" 597 infoText = "" 598 599 if iJSON is not None and iJSON and isinstance(iJSON, dict): 600 info = [ 601 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 602 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 603 "| Parameters | Values |\n", 604 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 605 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 606 "| Full name: | {:<54} |\n".format(iJSON["name"]), 607 ] 608 609 if "sector" in iJSON.keys() and iJSON["sector"]: 610 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 611 612 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 613 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 614 615 info.extend([ 616 splitLine, 617 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 618 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 619 ]) 620 621 if "isin" in iJSON.keys() and iJSON["isin"]: 622 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 623 624 if "classCode" in iJSON.keys(): 625 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 626 627 info.extend([ 628 splitLine, 629 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 630 splitLine, 631 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 632 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 633 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 634 ]) 635 636 if iJSON["figi"]: 637 self.figi = iJSON["figi"] 638 iJSON = iJSON | self.RequestTradingStatus() 639 640 info.extend([ 641 splitLine, 642 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 643 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 644 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 645 ]) 646 647 info.append(splitLine) 648 649 if "type" in iJSON.keys() and iJSON["type"]: 650 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 651 652 if "shareType" in iJSON.keys() and iJSON["shareType"]: 653 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 654 655 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 656 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 657 658 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 659 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 660 661 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 662 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 663 664 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 665 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 666 667 if "focusType" in iJSON.keys() and iJSON["focusType"]: 668 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 669 670 if "assetType" in iJSON.keys() and iJSON["assetType"]: 671 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 672 673 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 674 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 675 676 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 677 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 678 679 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 680 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 681 682 if "currency" in iJSON.keys(): 683 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 684 685 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 686 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 687 688 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 689 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 690 691 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 692 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 693 694 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 695 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 696 697 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 698 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 699 700 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 701 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 702 703 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 704 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 705 706 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 707 info.append("| Perpetual bond: | Yes |\n") 708 709 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 710 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 711 712 iExt = None 713 if iJSON["type"] == "Bonds": 714 info.extend([ 715 splitLine, 716 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 717 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 718 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 719 iJSON["nominal"]["currency"], 720 )), 721 ]) 722 723 if "floatingCouponFlag" in iJSON.keys(): 724 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 725 726 if "amortizationFlag" in iJSON.keys(): 727 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 728 729 info.append(splitLine) 730 731 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 732 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 733 734 if iJSON["figi"]: 735 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 736 737 info.extend([ 738 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 739 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 740 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 741 ]) 742 743 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 744 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 745 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 746 iJSON["aciValue"]["currency"] 747 ))) 748 749 if "currentPrice" in iJSON.keys(): 750 info.append(splitLine) 751 752 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 753 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 754 755 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 756 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 757 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 758 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 759 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 760 761 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 762 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 763 764 info.extend([ 765 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 766 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 767 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 768 )), 769 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 770 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 771 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 772 )), 773 "| Changes between last deal price and last close | {:<54} |\n".format( 774 "{:.2f}%{}".format( 775 iJSON["currentPrice"]["changes"], 776 " ({}{:.2f} {})".format( 777 "+" if bondChangesDelta > 0 else "", 778 bondChangesDelta, 779 aciCurrency 780 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 781 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 782 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 783 currency 784 ), 785 ) 786 ), 787 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 788 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 789 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 790 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 791 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 792 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 793 )), 794 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 795 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 796 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 797 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 798 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 799 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 800 )), 801 ]) 802 803 if "lot" in iJSON.keys(): 804 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 805 806 if "step" in iJSON.keys() and iJSON["step"] != 0: 807 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 808 809 # Add bond payment calendar: 810 if iJSON["type"] == "Bonds": 811 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 812 info.extend(["\n", strCalendar]) 813 814 infoText += "".join(info) 815 816 if show: 817 uLogger.info("{}".format(infoText)) 818 819 else: 820 uLogger.debug("{}".format(infoText)) 821 822 if self.infoFile is not None: 823 with open(self.infoFile, "w", encoding="UTF-8") as fH: 824 fH.write(infoText) 825 826 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 827 828 return infoText 829 830 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 831 """ 832 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 833 834 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 835 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 836 :return: JSON formatted data with information about instrument. 837 """ 838 tickerJSON = {} 839 if self.moreDebug: 840 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 841 842 if not self.ticker: 843 uLogger.warning("self.ticker variable is not be empty!") 844 845 else: 846 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 847 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 848 raise Exception("Instrument not allowed") 849 850 if not self.iList: 851 self.iList = self.Listing() 852 853 if self.ticker in self.iList["Shares"].keys(): 854 tickerJSON = self.iList["Shares"][self.ticker] 855 if self.moreDebug: 856 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 857 858 elif self.ticker in self.iList["Currencies"].keys(): 859 tickerJSON = self.iList["Currencies"][self.ticker] 860 if self.moreDebug: 861 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 862 863 elif self.ticker in self.iList["Bonds"].keys(): 864 tickerJSON = self.iList["Bonds"][self.ticker] 865 if self.moreDebug: 866 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 867 868 elif self.ticker in self.iList["Etfs"].keys(): 869 tickerJSON = self.iList["Etfs"][self.ticker] 870 if self.moreDebug: 871 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 872 873 elif self.ticker in self.iList["Futures"].keys(): 874 tickerJSON = self.iList["Futures"][self.ticker] 875 if self.moreDebug: 876 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 877 878 if tickerJSON: 879 self.figi = tickerJSON["figi"] 880 881 if requestPrice: 882 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 883 884 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 885 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 886 887 else: 888 tickerJSON["currentPrice"]["changes"] = 0 889 890 if show: 891 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 892 893 else: 894 if show: 895 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 896 897 return tickerJSON 898 899 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 900 """ 901 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 902 903 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 904 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 905 :return: JSON formatted data with information about instrument. 906 """ 907 figiJSON = {} 908 if self.moreDebug: 909 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 910 911 if not self.figi: 912 uLogger.warning("self.figi variable is not be empty!") 913 914 else: 915 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 916 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 917 raise Exception("Instrument not allowed") 918 919 if not self.iList: 920 self.iList = self.Listing() 921 922 for item in self.iList["Shares"].keys(): 923 if self.figi == self.iList["Shares"][item]["figi"]: 924 figiJSON = self.iList["Shares"][item] 925 926 if self.moreDebug: 927 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 928 929 break 930 931 if not figiJSON: 932 for item in self.iList["Currencies"].keys(): 933 if self.figi == self.iList["Currencies"][item]["figi"]: 934 figiJSON = self.iList["Currencies"][item] 935 936 if self.moreDebug: 937 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 938 939 break 940 941 if not figiJSON: 942 for item in self.iList["Bonds"].keys(): 943 if self.figi == self.iList["Bonds"][item]["figi"]: 944 figiJSON = self.iList["Bonds"][item] 945 946 if self.moreDebug: 947 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 948 949 break 950 951 if not figiJSON: 952 for item in self.iList["Etfs"].keys(): 953 if self.figi == self.iList["Etfs"][item]["figi"]: 954 figiJSON = self.iList["Etfs"][item] 955 956 if self.moreDebug: 957 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 958 959 break 960 961 if not figiJSON: 962 for item in self.iList["Futures"].keys(): 963 if self.figi == self.iList["Futures"][item]["figi"]: 964 figiJSON = self.iList["Futures"][item] 965 966 if self.moreDebug: 967 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 968 969 break 970 971 if figiJSON: 972 self.figi = figiJSON["figi"] 973 self.ticker = figiJSON["ticker"] 974 975 if requestPrice: 976 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 977 978 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 979 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 980 981 else: 982 figiJSON["currentPrice"]["changes"] = 0 983 984 if show: 985 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 986 987 else: 988 if show: 989 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 990 991 return figiJSON 992 993 def GetCurrentPrices(self, show: bool = True) -> dict: 994 """ 995 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 996 `{"buy": [{"price": 1243.8, "quantity": 193}, 997 {"price": 1244.0, "quantity": 168}, 998 {"price": 1244.8, "quantity": 5}, 999 {"price": 1245.0, "quantity": 61}, 1000 {"price": 1245.4, "quantity": 60}], 1001 "sell": [{"price": 1243.6, "quantity": 8}, 1002 {"price": 1242.6, "quantity": 10}, 1003 {"price": 1242.4, "quantity": 18}, 1004 {"price": 1242.2, "quantity": 50}, 1005 {"price": 1242.0, "quantity": 113}], 1006 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1007 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1008 - sell: list of dicts with Buyers prices, 1009 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1010 - quantity: volume value by current price in lots, 1011 - limitUp: current trade session limit price, maximum, 1012 - limitDown: current trade session limit price, minimum, 1013 - lastPrice: last deal price of the instrument, 1014 - closePrice: previous trade session close price of the instrument. 1015 1016 See also: `SearchByTicker()` and `SearchByFIGI()`. 1017 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1018 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1019 1020 :param show: if `True` then print DOM to log and console. 1021 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1022 If an error occurred then returns an empty record: 1023 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1024 """ 1025 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1026 1027 if self.depth < 1: 1028 uLogger.error("Depth of Market (DOM) must be >=1!") 1029 raise Exception("Incorrect value") 1030 1031 if not (self.ticker or self.figi): 1032 uLogger.error("self.ticker or self.figi variables must be defined!") 1033 raise Exception("Ticker or FIGI required") 1034 1035 if self.ticker and not self.figi: 1036 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1037 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1038 1039 if not self.ticker and self.figi: 1040 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1041 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1042 1043 if not self.figi: 1044 uLogger.error("FIGI is not defined!") 1045 raise Exception("Ticker or FIGI required") 1046 1047 else: 1048 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1049 1050 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1051 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1052 self.body = str({"figi": self.figi, "depth": self.depth}) 1053 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1054 1055 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1056 # list of dicts with sellers orders: 1057 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1058 1059 # list of dicts with buyers orders: 1060 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1061 1062 # max price of instrument at this time: 1063 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1064 1065 # min price of instrument at this time: 1066 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1067 1068 # last price of deal with instrument: 1069 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1070 1071 # last close price of instrument: 1072 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1073 1074 else: 1075 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1076 uLogger.debug("Server response: {}".format(pricesResponse)) 1077 1078 if show: 1079 if prices["buy"] or prices["sell"]: 1080 info = [ 1081 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1082 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1083 self.ticker, 1084 self.figi, 1085 self.depth, 1086 ), 1087 "-" * 60, "\n", 1088 " Orders of Buyers | Orders of Sellers\n", 1089 "-" * 60, "\n", 1090 " Sell prices (volumes) | Buy prices (volumes)\n", 1091 "-" * 60, "\n", 1092 ] 1093 1094 if not prices["buy"]: 1095 info.append(" | No orders!\n") 1096 sumBuy = 0 1097 1098 else: 1099 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1100 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1101 for item in maxMinSorted: 1102 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1103 1104 if not prices["sell"]: 1105 info.append("No orders! |\n") 1106 sumSell = 0 1107 1108 else: 1109 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1110 for item in prices["sell"]: 1111 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1112 1113 info.extend([ 1114 "-" * 60, "\n", 1115 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1116 "-" * 60, "\n", 1117 ]) 1118 1119 infoText = "".join(info) 1120 1121 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1122 1123 else: 1124 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1125 1126 return prices 1127 1128 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1129 """ 1130 This method get and show information about all available broker instruments for current user account. 1131 If `instrumentsFile` string is not empty then also save information to this file. 1132 1133 :param show: if `True` then print results to console, if `False` — print only to file. 1134 :return: multi-lines string with all available broker instruments 1135 """ 1136 if not self.iList: 1137 self.iList = self.Listing() 1138 1139 info = [ 1140 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1141 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1142 ] 1143 1144 # add instruments count by type: 1145 for iType in self.iList.keys(): 1146 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1147 1148 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1149 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1150 1151 # generating info tables with all instruments by type: 1152 for iType in self.iList.keys(): 1153 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1154 1155 for instrument in self.iList[iType].keys(): 1156 iName = self.iList[iType][instrument]["name"] # instrument's name 1157 if len(iName) > 57: 1158 iName = "{}...".format(iName[:54]) # right trim for a long string 1159 1160 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1161 self.iList[iType][instrument]["ticker"], 1162 iName, 1163 self.iList[iType][instrument]["figi"], 1164 self.iList[iType][instrument]["currency"], 1165 self.iList[iType][instrument]["lot"], 1166 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1167 )) 1168 1169 infoText = "".join(info) 1170 1171 if show: 1172 uLogger.info(infoText) 1173 1174 if self.instrumentsFile: 1175 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1176 fH.write(infoText) 1177 1178 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1179 1180 return infoText 1181 1182 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1183 """ 1184 This method search and show information about instruments by part of its ticker, FIGI or name. 1185 If `searchResultsFile` string is not empty then also save information to this file. 1186 1187 :param pattern: string with part of ticker, FIGI or instrument's name. 1188 :param show: if `True` then print results to console, if `False` — return list of result only. 1189 :return: list of dictionaries with all found instruments. 1190 """ 1191 if not self.iList: 1192 self.iList = self.Listing() 1193 1194 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1195 compiledPattern = re.compile(pattern, re.IGNORECASE) 1196 1197 for iType in self.iList: 1198 for instrument in self.iList[iType].values(): 1199 searchResult = compiledPattern.search(" ".join( 1200 [instrument["ticker"], instrument["figi"], instrument["name"]] 1201 )) 1202 1203 if searchResult: 1204 searchResults[iType][instrument["ticker"]] = instrument 1205 1206 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1207 info = [ 1208 "# Search results\n\n", 1209 "* **Search pattern:** [{}]\n".format(pattern), 1210 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1211 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1212 ] 1213 infoShort = info[:] 1214 1215 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1216 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1217 skippedLine = "| ... | ... | ... | ... |\n" 1218 1219 if resultsLen == 0: 1220 info.append("\nNo results\n") 1221 infoShort.append("\nNo results\n") 1222 uLogger.warning("No results. Try changing your search pattern.") 1223 1224 else: 1225 for iType in searchResults: 1226 iTypeValuesCount = len(searchResults[iType].values()) 1227 if iTypeValuesCount > 0: 1228 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1229 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1230 1231 for instrument in searchResults[iType].values(): 1232 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1233 instrument["type"], 1234 instrument["ticker"], 1235 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1236 instrument["figi"], 1237 )) 1238 1239 if iTypeValuesCount <= 5: 1240 infoShort.extend(info[-iTypeValuesCount:]) 1241 1242 else: 1243 infoShort.extend(info[-5:]) 1244 infoShort.append(skippedLine) 1245 1246 infoText = "".join(info) 1247 infoTextShort = "".join(infoShort) 1248 1249 if show: 1250 uLogger.info(infoTextShort) 1251 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1252 1253 if self.searchResultsFile: 1254 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1255 fH.write(infoText) 1256 1257 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1258 1259 return searchResults 1260 1261 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1262 """ 1263 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1264 1265 :param instruments: list of strings with tickers or FIGIs. 1266 :return: list with unique instrument FIGIs only. 1267 """ 1268 requestedInstruments = [] 1269 for iName in instruments: 1270 if iName not in self.aliases.keys(): 1271 if iName not in requestedInstruments: 1272 requestedInstruments.append(iName) 1273 1274 else: 1275 if iName not in requestedInstruments: 1276 if self.aliases[iName] not in requestedInstruments: 1277 requestedInstruments.append(self.aliases[iName]) 1278 1279 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1280 1281 onlyUniqueFIGIs = [] 1282 for iName in requestedInstruments: 1283 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1284 continue 1285 1286 self.ticker = iName 1287 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1288 1289 if not iData: 1290 self.ticker = "" 1291 self.figi = iName 1292 1293 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1294 1295 if not iData: 1296 self.figi = "" 1297 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1298 1299 if iData and iData["figi"] not in onlyUniqueFIGIs: 1300 onlyUniqueFIGIs.append(iData["figi"]) 1301 1302 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1303 1304 return onlyUniqueFIGIs 1305 1306 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1307 """ 1308 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1309 1310 See limits: https://tinkoff.github.io/investAPI/limits/ 1311 1312 If `pricesFile` string is not empty then also save information to this file. 1313 1314 :param instruments: list of strings with tickers or FIGIs. 1315 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1316 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1317 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1318 """ 1319 if instruments is None or not instruments: 1320 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1321 raise Exception("Ticker or FIGI required") 1322 1323 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1324 1325 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1326 1327 iList = [] # trying to get info and current prices about all unique instruments: 1328 for self.figi in onlyUniqueFIGIs: 1329 iData = self.SearchByFIGI(requestPrice=True) 1330 iList.append(iData) 1331 1332 self.ShowListOfPrices(iList, show) 1333 1334 return iList 1335 1336 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1337 """ 1338 Show table contains current prices of given instruments. 1339 1340 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1341 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1342 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1343 :return: multilines text in Markdown format as a table contains current prices. 1344 """ 1345 infoText = "" 1346 1347 if show or self.pricesFile: 1348 info = [ 1349 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1350 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1351 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1352 ] 1353 1354 for item in iList: 1355 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1356 item["ticker"], 1357 item["figi"], 1358 item["type"], 1359 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1360 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1361 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1362 "{} / {}".format( 1363 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1364 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1365 ), 1366 "{} / {}".format( 1367 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1368 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1369 ), 1370 item["currency"], 1371 )) 1372 1373 infoText = "".join(info) 1374 1375 if show: 1376 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1377 1378 if self.pricesFile: 1379 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1380 fH.write(infoText) 1381 1382 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1383 1384 return infoText 1385 1386 def RequestTradingStatus(self) -> dict: 1387 """ 1388 Requesting trading status for the instrument defined by `figi` variable. 1389 1390 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1391 1392 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1393 1394 :return: dictionary with trading status attributes. Response example: 1395 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1396 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1397 """ 1398 if self.figi is None or not self.figi: 1399 uLogger.error("Variable `figi` must be defined for using this method!") 1400 raise Exception("FIGI required") 1401 1402 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1403 1404 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1405 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1406 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1407 1408 if self.moreDebug: 1409 uLogger.debug("Records about current trading status successfully received") 1410 1411 return tradingStatus 1412 1413 def RequestPortfolio(self) -> dict: 1414 """ 1415 Requesting actual user's portfolio for current `accountId`. 1416 1417 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1418 1419 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1420 1421 :return: dictionary with user's portfolio. 1422 """ 1423 if self.accountId is None or not self.accountId: 1424 uLogger.error("Variable `accountId` must be defined for using this method!") 1425 raise Exception("Account ID required") 1426 1427 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1428 1429 self.body = str({"accountId": self.accountId}) 1430 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1431 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1432 1433 if self.moreDebug: 1434 uLogger.debug("Records about user's portfolio successfully received") 1435 1436 return rawPortfolio 1437 1438 def RequestPositions(self) -> dict: 1439 """ 1440 Requesting open positions by currencies and instruments for current `accountId`. 1441 1442 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1443 1444 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1445 1446 :return: dictionary with open positions by instruments. 1447 """ 1448 if self.accountId is None or not self.accountId: 1449 uLogger.error("Variable `accountId` must be defined for using this method!") 1450 raise Exception("Account ID required") 1451 1452 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1453 1454 self.body = str({"accountId": self.accountId}) 1455 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1456 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1457 1458 if self.moreDebug: 1459 uLogger.debug("Records about current open positions successfully received") 1460 1461 return rawPositions 1462 1463 def RequestPendingOrders(self) -> list: 1464 """ 1465 Requesting current actual pending orders for current `accountId`. 1466 1467 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1468 1469 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1470 1471 :return: list of dictionaries with pending orders. 1472 """ 1473 if self.accountId is None or not self.accountId: 1474 uLogger.error("Variable `accountId` must be defined for using this method!") 1475 raise Exception("Account ID required") 1476 1477 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1478 1479 self.body = str({"accountId": self.accountId}) 1480 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1481 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1482 1483 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1484 1485 return rawOrders 1486 1487 def RequestStopOrders(self) -> list: 1488 """ 1489 Requesting current actual stop orders for current `accountId`. 1490 1491 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1492 1493 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1494 1495 :return: list of dictionaries with stop orders. 1496 """ 1497 if self.accountId is None or not self.accountId: 1498 uLogger.error("Variable `accountId` must be defined for using this method!") 1499 raise Exception("Account ID required") 1500 1501 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1502 1503 self.body = str({"accountId": self.accountId}) 1504 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1505 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1506 1507 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1508 1509 return rawStopOrders 1510 1511 def Overview(self, show: bool = False, details: str = "full") -> dict: 1512 """ 1513 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1514 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1515 and `overviewBondsCalendarFile` are defined then also save information to file. 1516 1517 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1518 many requests about the state of the portfolio, and then, based on the received data, a large number 1519 of calculation and statistics are collected. 1520 1521 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1522 :param details: how detailed should the information be? 1523 - `full` — shows full available information about portfolio status (by default), 1524 - `positions` — shows only open positions, 1525 - `orders` — shows only sections of open limits and stop orders. 1526 - `digest` — show a short digest of the portfolio status, 1527 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1528 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1529 :return: dictionary with client's raw portfolio and some statistics. 1530 """ 1531 if self.accountId is None or not self.accountId: 1532 uLogger.error("Variable `accountId` must be defined for using this method!") 1533 raise Exception("Account ID required") 1534 1535 view = { 1536 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1537 "headers": {}, # list of dictionaries, response headers without "positions" section 1538 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1539 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1540 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1541 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1542 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1543 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1544 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1545 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1546 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1547 }, 1548 "stat": { # --- some statistics calculated using "raw" sections: 1549 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1550 "availableRUB": 0., # available rubles (without other currencies) 1551 "blockedRUB": 0., # blocked sum in Russian Rouble 1552 "totalChangesRUB": 0., # changes for all open trades in RUB 1553 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1554 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1555 "sharesCostRUB": 0., # costs of all shares in RUB 1556 "bondsCostRUB": 0., # costs of all bonds in RUB 1557 "etfsCostRUB": 0., # costs of all etfs in RUB 1558 "futuresCostRUB": 0., # costs of all futures in RUB 1559 "Currencies": [], # list of dictionaries of all currencies statistics 1560 "Shares": [], # list of dictionaries of all shares statistics 1561 "Bonds": [], # list of dictionaries of all bonds statistics 1562 "Etfs": [], # list of dictionaries of all etfs statistics 1563 "Futures": [], # list of dictionaries of all futures statistics 1564 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1565 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1566 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1567 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1568 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1569 }, 1570 "analytics": { # --- some analytics of portfolio: 1571 "distrByAssets": {}, # portfolio distribution by assets 1572 "distrByCompanies": {}, # portfolio distribution by companies 1573 "distrBySectors": {}, # portfolio distribution by sectors 1574 "distrByCurrencies": {}, # portfolio distribution by currencies 1575 "distrByCountries": {}, # portfolio distribution by countries 1576 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1577 } 1578 } 1579 1580 details = details.lower() 1581 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1582 if details not in availableDetails: 1583 details = "full" 1584 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1585 1586 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1587 1588 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1589 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1590 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1591 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1592 1593 # save response headers without "positions" section: 1594 for key in portfolioResponse.keys(): 1595 if key != "positions": 1596 view["raw"]["headers"][key] = portfolioResponse[key] 1597 1598 else: 1599 continue 1600 1601 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1602 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1603 for item in portfolioResponse["positions"]: 1604 if item["instrumentType"] == "currency": 1605 self.figi = item["figi"] 1606 curr = self.SearchByFIGI(requestPrice=False) 1607 1608 # current price of currency in RUB: 1609 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1610 "name": curr["name"], 1611 "currentPrice": NanoToFloat( 1612 item["currentPrice"]["units"], 1613 item["currentPrice"]["nano"] 1614 ), 1615 } 1616 1617 view["raw"]["Currencies"].append(item) 1618 1619 elif item["instrumentType"] == "share": 1620 view["raw"]["Shares"].append(item) 1621 1622 elif item["instrumentType"] == "bond": 1623 view["raw"]["Bonds"].append(item) 1624 1625 elif item["instrumentType"] == "etf": 1626 view["raw"]["Etfs"].append(item) 1627 1628 elif item["instrumentType"] == "futures": 1629 view["raw"]["Futures"].append(item) 1630 1631 else: 1632 continue 1633 1634 # how many volume of currencies (by ISO currency name) are blocked: 1635 for item in view["raw"]["positions"]["blocked"]: 1636 blocked = NanoToFloat(item["units"], item["nano"]) 1637 if blocked > 0: 1638 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1639 1640 # how many volume of instruments (by FIGI) are blocked: 1641 for item in view["raw"]["positions"]["securities"]: 1642 blocked = int(item["blocked"]) 1643 if blocked > 0: 1644 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1645 1646 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1647 1648 if "rub" in allBlocked.keys(): 1649 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1650 1651 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1652 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1653 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1654 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1655 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1656 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1657 view["stat"]["portfolioCostRUB"] = sum([ 1658 view["stat"]["allCurrenciesCostRUB"], 1659 view["stat"]["sharesCostRUB"], 1660 view["stat"]["bondsCostRUB"], 1661 view["stat"]["etfsCostRUB"], 1662 view["stat"]["futuresCostRUB"], 1663 ]) 1664 1665 # --- calculating some portfolio statistics: 1666 byComp = {} # distribution by companies 1667 bySect = {} # distribution by sectors 1668 byCurr = {} # distribution by currencies (include RUB) 1669 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1670 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1671 1672 for item in portfolioResponse["positions"]: 1673 self.figi = item["figi"] 1674 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1675 1676 if instrument: 1677 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1678 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1679 1680 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1681 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1682 1683 else: 1684 blocked = 0 1685 1686 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1687 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1688 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1689 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1690 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1691 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1692 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1693 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1694 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1695 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1696 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1697 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1698 1699 statData = { 1700 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1701 "ticker": instrument["ticker"], # ticker by FIGI 1702 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1703 "volume": volume, # available volume of instrument 1704 "lots": lots, # volume in lots of instrument 1705 "direction": direction, # direction of an instrument's position: short or long 1706 "blocked": blocked, # blocked volume of currency or instrument 1707 "currentPrice": curPrice, # current instrument's price in basic asset 1708 "average": average, # current average position price 1709 "cost": cost, # current cost of all volume of instrument in basic asset 1710 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1711 "costRUB": costRUB, # cost of instrument in ruble 1712 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1713 "profit": profit, # expected profit at current moment 1714 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1715 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1716 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1717 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1718 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1719 "step": instrument["step"], # minimum price increment 1720 } 1721 1722 # adding distribution by unique countries: 1723 if statData["country"] not in byCountry.keys(): 1724 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1725 1726 else: 1727 byCountry[statData["country"]]["cost"] += costRUB 1728 byCountry[statData["country"]]["percent"] += percentCostRUB 1729 1730 if item["instrumentType"] != "currency": 1731 # adding distribution by unique companies: 1732 if statData["name"]: 1733 if statData["name"] not in byComp.keys(): 1734 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1735 1736 else: 1737 byComp[statData["name"]]["cost"] += costRUB 1738 byComp[statData["name"]]["percent"] += percentCostRUB 1739 1740 # adding distribution by unique sectors: 1741 if statData["sector"] not in bySect.keys(): 1742 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1743 1744 else: 1745 bySect[statData["sector"]]["cost"] += costRUB 1746 bySect[statData["sector"]]["percent"] += percentCostRUB 1747 1748 # adding distribution by unique currencies: 1749 if currency not in byCurr.keys(): 1750 byCurr[currency] = { 1751 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1752 "cost": costRUB, 1753 "percent": percentCostRUB 1754 } 1755 1756 else: 1757 byCurr[currency]["cost"] += costRUB 1758 byCurr[currency]["percent"] += percentCostRUB 1759 1760 # saving statistics for every instrument: 1761 if item["instrumentType"] == "currency": 1762 view["stat"]["Currencies"].append(statData) 1763 1764 # update dict with free funds for trading (total - blocked) by currencies 1765 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1766 view["stat"]["funds"][currency] = { 1767 "total": volume, 1768 "totalCostRUB": costRUB, # total volume cost in rubles 1769 "free": volume - blocked, 1770 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1771 } 1772 1773 elif item["instrumentType"] == "share": 1774 view["stat"]["Shares"].append(statData) 1775 1776 elif item["instrumentType"] == "bond": 1777 view["stat"]["Bonds"].append(statData) 1778 1779 elif item["instrumentType"] == "etf": 1780 view["stat"]["Etfs"].append(statData) 1781 1782 elif item["instrumentType"] == "Futures": 1783 view["stat"]["Futures"].append(statData) 1784 1785 else: 1786 continue 1787 1788 # total changes in Russian Ruble: 1789 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1790 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1791 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1792 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1793 view["stat"]["funds"]["rub"] = { 1794 "total": view["stat"]["availableRUB"], 1795 "totalCostRUB": view["stat"]["availableRUB"], 1796 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1797 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1798 } 1799 1800 # --- pending orders sector data: 1801 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending orders to avoid many times price requests 1802 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1803 1804 for item in view["raw"]["orders"]: 1805 self.figi = item["figi"] 1806 1807 if item["figi"] not in uniquePendingOrdersFIGIs: 1808 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1809 1810 uniquePendingOrdersFIGIs.append(item["figi"]) 1811 uniquePendingOrders[item["figi"]] = instrument 1812 1813 else: 1814 instrument = uniquePendingOrders[item["figi"]] 1815 1816 if instrument: 1817 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1818 orderType = TKS_ORDER_TYPES[item["orderType"]] 1819 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1820 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1821 1822 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1823 if item["direction"] == "ORDER_DIRECTION_BUY": 1824 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1825 1826 else: 1827 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1828 1829 # requested price for order execution: 1830 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1831 1832 # necessary changes in percent to reach target from current price: 1833 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1834 1835 view["stat"]["orders"].append({ 1836 "orderID": item["orderId"], # orderId number parameter of current order 1837 "figi": item["figi"], # FIGI identification 1838 "ticker": instrument["ticker"], # ticker name by FIGI 1839 "lotsRequested": item["lotsRequested"], # requested lots value 1840 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1841 "currentPrice": lastPrice, # current instrument's price for defined action 1842 "targetPrice": target, # requested price for order execution in base currency 1843 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1844 "percentChanges": changes, # changes in percent to target from current price 1845 "currency": item["currency"], # instrument's currency name 1846 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1847 "type": orderType, # type of order from TKS_ORDER_TYPES 1848 "status": orderState, # order status from TKS_ORDER_STATES 1849 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1850 }) 1851 1852 # --- stop orders sector data: 1853 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1854 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1855 1856 for item in view["raw"]["stopOrders"]: 1857 self.figi = item["figi"] 1858 1859 if item["figi"] not in uniqueStopOrdersFIGIs: 1860 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1861 1862 uniqueStopOrdersFIGIs.append(item["figi"]) 1863 uniqueStopOrders[item["figi"]] = instrument 1864 1865 else: 1866 instrument = uniqueStopOrders[item["figi"]] 1867 1868 if instrument: 1869 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1870 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1871 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1872 1873 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1874 if "expirationTime" in item.keys(): 1875 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1876 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1877 1878 else: 1879 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1880 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1881 1882 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1883 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1884 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1885 1886 else: 1887 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1888 1889 # requested price when stop-order executed: 1890 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1891 1892 # price for limit-order, set up when stop-order executed: 1893 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1894 1895 # necessary changes in percent to reach target from current price: 1896 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1897 1898 view["stat"]["stopOrders"].append({ 1899 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1900 "figi": item["figi"], # FIGI identification 1901 "ticker": instrument["ticker"], # ticker name by FIGI 1902 "lotsRequested": item["lotsRequested"], # requested lots value 1903 "currentPrice": lastPrice, # current instrument's price for defined action 1904 "targetPrice": target, # requested price for stop-order execution in base currency 1905 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1906 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1907 "percentChanges": changes, # changes in percent to target from current price 1908 "currency": item["currency"], # instrument's currency name 1909 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1910 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1911 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1912 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1913 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1914 }) 1915 1916 # --- calculating data for analytics section: 1917 # portfolio distribution by assets: 1918 view["analytics"]["distrByAssets"] = { 1919 "Ruble": { 1920 "uniques": 1, 1921 "cost": view["stat"]["availableRUB"], 1922 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1923 }, 1924 "Currencies": { 1925 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 1926 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 1927 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1928 }, 1929 "Shares": { 1930 "uniques": len(view["stat"]["Shares"]), 1931 "cost": view["stat"]["sharesCostRUB"], 1932 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1933 }, 1934 "Bonds": { 1935 "uniques": len(view["stat"]["Bonds"]), 1936 "cost": view["stat"]["bondsCostRUB"], 1937 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1938 }, 1939 "Etfs": { 1940 "uniques": len(view["stat"]["Etfs"]), 1941 "cost": view["stat"]["etfsCostRUB"], 1942 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1943 }, 1944 "Futures": { 1945 "uniques": len(view["stat"]["Futures"]), 1946 "cost": view["stat"]["futuresCostRUB"], 1947 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1948 }, 1949 } 1950 1951 # portfolio distribution by companies: 1952 view["analytics"]["distrByCompanies"]["All money cash"] = { 1953 "ticker": "", 1954 "cost": view["stat"]["allCurrenciesCostRUB"], 1955 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1956 } 1957 view["analytics"]["distrByCompanies"].update(byComp) 1958 1959 # portfolio distribution by sectors: 1960 view["analytics"]["distrBySectors"]["All money cash"] = { 1961 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 1962 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 1963 } 1964 view["analytics"]["distrBySectors"].update(bySect) 1965 1966 # portfolio distribution by currencies: 1967 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 1968 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 1969 1970 if self.moreDebug: 1971 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 1972 1973 view["analytics"]["distrByCurrencies"].update(byCurr) 1974 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 1975 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 1976 1977 # portfolio distribution by countries: 1978 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 1979 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 1980 1981 if self.moreDebug: 1982 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 1983 1984 view["analytics"]["distrByCountries"].update(byCountry) 1985 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 1986 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 1987 1988 # --- Prepare text statistics overview in human-readable: 1989 if show: 1990 # Whatever the value `details`, header not changes: 1991 info = [ 1992 "# Client's portfolio\n\n", 1993 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1994 "* **Account ID:** [{}]\n".format(self.accountId), 1995 ] 1996 1997 if details in ["full", "positions", "digest"]: 1998 info.extend([ 1999 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2000 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2001 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2002 view["stat"]["totalChangesRUB"], 2003 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2004 view["stat"]["totalChangesPercentRUB"], 2005 ), 2006 ]) 2007 2008 if details in ["full", "positions"]: 2009 info.extend([ 2010 "## Open positions\n\n", 2011 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2012 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2013 "| Ruble | {:>31} | | | | | |\n".format( 2014 "{:.2f} ({:.2f}) rub".format( 2015 view["stat"]["availableRUB"], 2016 view["stat"]["blockedRUB"], 2017 ) 2018 ) 2019 ]) 2020 2021 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2022 return [ 2023 "| | | | | | | |\n", 2024 "| {:<27} | | | | | {:>19} | |\n".format( 2025 noTradeStr if noTradeStr else typeStr, 2026 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2027 ), 2028 ] 2029 2030 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2031 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2032 "{} [{}]".format(data["ticker"], data["figi"]), 2033 "{:.2f} ({:.2f}) {}".format( 2034 data["volume"], 2035 data["blocked"], 2036 data["currency"], 2037 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2038 data["volume"], 2039 data["blocked"], 2040 ), 2041 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2042 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2043 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2044 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2045 "{}{:.2f} {} ({}{:.2f}%)".format( 2046 "+" if data["profit"] > 0 else "", 2047 data["profit"], data["baseCurrencyName"], 2048 "+" if data["percentProfit"] > 0 else "", 2049 data["percentProfit"], 2050 ), 2051 ) 2052 2053 # --- Show currencies section: 2054 if view["stat"]["Currencies"]: 2055 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2056 for item in view["stat"]["Currencies"]: 2057 info.append(_InfoStr(item, showCurrencyName=True)) 2058 2059 else: 2060 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2061 2062 # --- Show shares section: 2063 if view["stat"]["Shares"]: 2064 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2065 2066 for item in view["stat"]["Shares"]: 2067 info.append(_InfoStr(item)) 2068 2069 else: 2070 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2071 2072 # --- Show bonds section: 2073 if view["stat"]["Bonds"]: 2074 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2075 2076 for item in view["stat"]["Bonds"]: 2077 info.append(_InfoStr(item)) 2078 2079 else: 2080 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2081 2082 # --- Show etfs section: 2083 if view["stat"]["Etfs"]: 2084 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2085 2086 for item in view["stat"]["Etfs"]: 2087 info.append(_InfoStr(item)) 2088 2089 else: 2090 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2091 2092 # --- Show futures section: 2093 if view["stat"]["Futures"]: 2094 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2095 2096 for item in view["stat"]["Futures"]: 2097 info.append(_InfoStr(item)) 2098 2099 else: 2100 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2101 2102 if details in ["full", "orders"]: 2103 # --- Show pending orders section: 2104 if view["stat"]["orders"]: 2105 info.extend([ 2106 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2107 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2108 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2109 ]) 2110 2111 for item in view["stat"]["orders"]: 2112 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2113 "{} [{}]".format(item["ticker"], item["figi"]), 2114 item["orderID"], 2115 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2116 "{} {} ({}{:.2f}%)".format( 2117 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2118 item["baseCurrencyName"], 2119 "+" if item["percentChanges"] > 0 else "", 2120 float(item["percentChanges"]), 2121 ), 2122 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2123 item["action"], 2124 item["type"], 2125 item["date"], 2126 )) 2127 2128 else: 2129 info.append("\n## Total pending limit-orders: 0\n") 2130 2131 # --- Show stop orders section: 2132 if view["stat"]["stopOrders"]: 2133 info.extend([ 2134 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2135 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2136 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2137 ]) 2138 2139 for item in view["stat"]["stopOrders"]: 2140 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2141 "{} [{}]".format(item["ticker"], item["figi"]), 2142 item["orderID"], 2143 item["lotsRequested"], 2144 "{} {} ({}{:.2f}%)".format( 2145 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2146 item["baseCurrencyName"], 2147 "+" if item["percentChanges"] > 0 else "", 2148 float(item["percentChanges"]), 2149 ), 2150 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2151 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2152 item["action"], 2153 item["type"], 2154 item["expType"], 2155 item["createDate"], 2156 item["expDate"], 2157 )) 2158 2159 else: 2160 info.append("\n## Total stop-orders: 0\n") 2161 2162 if details in ["full", "analytics"]: 2163 # -- Show analytics section: 2164 if view["stat"]["portfolioCostRUB"] > 0: 2165 info.extend([ 2166 "\n# Analytics\n" 2167 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2168 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2169 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2170 view["stat"]["totalChangesRUB"], 2171 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2172 view["stat"]["totalChangesPercentRUB"], 2173 ), 2174 "\n## Portfolio distribution by assets\n" 2175 "\n| Type | Uniques | Percent | Current cost |\n", 2176 "|------------------------------------|---------|---------|--------------------|\n", 2177 ]) 2178 2179 for key in view["analytics"]["distrByAssets"].keys(): 2180 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2181 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2182 key, 2183 view["analytics"]["distrByAssets"][key]["uniques"], 2184 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2185 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2186 )) 2187 2188 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2189 2190 info.extend([ 2191 "\n## Portfolio distribution by companies\n" 2192 "\n| Company | Percent | Current cost |\n", 2193 aSepLine, 2194 ]) 2195 2196 for company in view["analytics"]["distrByCompanies"].keys(): 2197 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2198 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2199 "{}{}".format( 2200 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2201 company, 2202 ), 2203 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2204 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2205 )) 2206 2207 info.extend([ 2208 "\n## Portfolio distribution by sectors\n" 2209 "\n| Sector | Percent | Current cost |\n", 2210 aSepLine, 2211 ]) 2212 2213 for sector in view["analytics"]["distrBySectors"].keys(): 2214 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2215 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2216 sector, 2217 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2218 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2219 )) 2220 2221 info.extend([ 2222 "\n## Portfolio distribution by currencies\n" 2223 "\n| Instruments currencies | Percent | Current cost |\n", 2224 aSepLine, 2225 ]) 2226 2227 for curr in view["analytics"]["distrByCurrencies"].keys(): 2228 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2229 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2230 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2231 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2232 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2233 )) 2234 2235 info.extend([ 2236 "\n## Portfolio distribution by countries\n" 2237 "\n| Assets by country | Percent | Current cost |\n", 2238 aSepLine, 2239 ]) 2240 2241 for country in view["analytics"]["distrByCountries"].keys(): 2242 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2243 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2244 country, 2245 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2246 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2247 )) 2248 2249 if details in ["full", "calendar"]: 2250 # -- Show bonds payment calendar section: 2251 if view["stat"]["Bonds"]: 2252 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2253 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2254 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2255 2256 else: 2257 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2258 2259 infoText = "".join(info) 2260 2261 uLogger.info(infoText) 2262 2263 if details == "full" and self.overviewFile: 2264 filename = self.overviewFile 2265 2266 elif details == "digest" and self.overviewDigestFile: 2267 filename = self.overviewDigestFile 2268 2269 elif details == "positions" and self.overviewPositionsFile: 2270 filename = self.overviewPositionsFile 2271 2272 elif details == "orders" and self.overviewOrdersFile: 2273 filename = self.overviewOrdersFile 2274 2275 elif details == "analytics" and self.overviewAnalyticsFile: 2276 filename = self.overviewAnalyticsFile 2277 2278 elif details == "calendar" and self.overviewBondsCalendarFile: 2279 filename = self.overviewBondsCalendarFile 2280 2281 else: 2282 filename = "" 2283 2284 if filename: 2285 with open(filename, "w", encoding="UTF-8") as fH: 2286 fH.write(infoText) 2287 2288 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2289 2290 return view 2291 2292 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2293 """ 2294 Returns history operations between two given dates for current `accountId`. 2295 If `reportFile` string is not empty then also save human-readable report. 2296 Shows some statistical data of closed positions. 2297 2298 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2299 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2300 :param show: if `True` then also prints all records to the console. 2301 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2302 :return: original list of dictionaries with history of deals records from API ("operations" key): 2303 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2304 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2305 """ 2306 if self.accountId is None or not self.accountId: 2307 uLogger.error("Variable `accountId` must be defined for using this method!") 2308 raise Exception("Account ID required") 2309 2310 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2311 2312 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2313 2314 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2315 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2316 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2317 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2318 customStat = {} # custom statistics in additional to responseJSON 2319 2320 # --- output report in human-readable format: 2321 if show or self.reportFile: 2322 splitLine1 = "| | | | | |\n" # Summary section 2323 splitLine2 = "| | | | | | | | |\n" # Operations section 2324 nextDay = "" 2325 2326 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2327 2328 if len(ops) > 0: 2329 customStat = { 2330 "opsCount": 0, # total operations count 2331 "buyCount": 0, # buy operations 2332 "sellCount": 0, # sell operations 2333 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2334 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2335 "payIn": {"rub": 0.}, # Deposit brokerage account 2336 "payOut": {"rub": 0.}, # Withdrawals 2337 "divs": {"rub": 0.}, # Dividends income 2338 "coupons": {"rub": 0.}, # Coupon's income 2339 "brokerCom": {"rub": 0.}, # Service commissions 2340 "serviceCom": {"rub": 0.}, # Service commissions 2341 "marginCom": {"rub": 0.}, # Margin commissions 2342 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2343 } 2344 2345 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2346 for item in ops: 2347 if item["state"] == "OPERATION_STATE_EXECUTED": 2348 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2349 2350 # count buy operations: 2351 if "_BUY" in item["operationType"]: 2352 customStat["buyCount"] += 1 2353 2354 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2355 customStat["buyTotal"][item["payment"]["currency"]] += payment 2356 2357 else: 2358 customStat["buyTotal"][item["payment"]["currency"]] = payment 2359 2360 # count sell operations: 2361 elif "_SELL" in item["operationType"]: 2362 customStat["sellCount"] += 1 2363 2364 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2365 customStat["sellTotal"][item["payment"]["currency"]] += payment 2366 2367 else: 2368 customStat["sellTotal"][item["payment"]["currency"]] = payment 2369 2370 # count incoming operations: 2371 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2372 if item["payment"]["currency"] in customStat["payIn"].keys(): 2373 customStat["payIn"][item["payment"]["currency"]] += payment 2374 2375 else: 2376 customStat["payIn"][item["payment"]["currency"]] = payment 2377 2378 # count withdrawals operations: 2379 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2380 if item["payment"]["currency"] in customStat["payOut"].keys(): 2381 customStat["payOut"][item["payment"]["currency"]] += payment 2382 2383 else: 2384 customStat["payOut"][item["payment"]["currency"]] = payment 2385 2386 # count dividends income: 2387 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2388 if item["payment"]["currency"] in customStat["divs"].keys(): 2389 customStat["divs"][item["payment"]["currency"]] += payment 2390 2391 else: 2392 customStat["divs"][item["payment"]["currency"]] = payment 2393 2394 # count coupon's income: 2395 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2396 if item["payment"]["currency"] in customStat["coupons"].keys(): 2397 customStat["coupons"][item["payment"]["currency"]] += payment 2398 2399 else: 2400 customStat["coupons"][item["payment"]["currency"]] = payment 2401 2402 # count broker commissions: 2403 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2404 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2405 customStat["brokerCom"][item["payment"]["currency"]] += payment 2406 2407 else: 2408 customStat["brokerCom"][item["payment"]["currency"]] = payment 2409 2410 # count service commissions: 2411 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2412 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2413 customStat["serviceCom"][item["payment"]["currency"]] += payment 2414 2415 else: 2416 customStat["serviceCom"][item["payment"]["currency"]] = payment 2417 2418 # count margin commissions: 2419 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2420 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2421 customStat["marginCom"][item["payment"]["currency"]] += payment 2422 2423 else: 2424 customStat["marginCom"][item["payment"]["currency"]] = payment 2425 2426 # count withholding taxes: 2427 elif "_TAX" in item["operationType"]: 2428 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2429 customStat["allTaxes"][item["payment"]["currency"]] += payment 2430 2431 else: 2432 customStat["allTaxes"][item["payment"]["currency"]] = payment 2433 2434 else: 2435 continue 2436 2437 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2438 2439 # --- view "Actions" lines: 2440 info.extend([ 2441 "| Report sections | | | | |\n", 2442 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2443 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2444 "| | Buy: {:<22} | {:<28} | | |\n".format( 2445 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2446 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2447 ), 2448 "| | Sell: {:<21} | {:<28} | | |\n".format( 2449 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2450 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2451 ), 2452 ]) 2453 2454 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2455 for key in opsKeys: 2456 if key == "rub": 2457 continue 2458 2459 info.extend([ 2460 "| | | {:<28} | | |\n".format( 2461 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2462 ), 2463 "| | | {:<28} | | |\n".format( 2464 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2465 ), 2466 ]) 2467 2468 info.append(splitLine1) 2469 2470 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2471 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2472 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2473 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2474 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2475 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2476 ) 2477 2478 # --- view "Payments" lines: 2479 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2480 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2481 2482 for key in paymentsKeys: 2483 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2484 2485 info.append(splitLine1) 2486 2487 # --- view "Commissions and taxes" lines: 2488 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2489 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2490 2491 for key in comKeys: 2492 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2493 2494 info.append(splitLine1) 2495 2496 info.extend([ 2497 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2498 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2499 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2500 ]) 2501 2502 else: 2503 info.append("Broker returned no operations during this period\n") 2504 2505 # --- view "Operations" section: 2506 for item in ops: 2507 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2508 continue 2509 2510 else: 2511 self.figi = item["figi"] if item["figi"] else "" 2512 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2513 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2514 2515 # group of deals during one day: 2516 if nextDay and item["date"].split("T")[0] != nextDay: 2517 info.append(splitLine2) 2518 nextDay = "" 2519 2520 else: 2521 nextDay = item["date"].split("T")[0] # saving current day for splitting 2522 2523 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2524 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2525 self.figi if self.figi else "—", 2526 instrument["ticker"] if instrument else "—", 2527 instrument["type"] if instrument else "—", 2528 item["quantity"] if int(item["quantity"]) > 0 else "—", 2529 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2530 TKS_OPERATION_STATES[item["state"]], 2531 TKS_OPERATION_TYPES[item["operationType"]], 2532 )) 2533 2534 infoText = "".join(info) 2535 2536 if show: 2537 if self.moreDebug: 2538 uLogger.debug("Records about history of a client's operations successfully received") 2539 2540 uLogger.info(infoText) 2541 2542 if self.reportFile: 2543 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2544 fH.write(infoText) 2545 2546 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2547 2548 return ops, customStat 2549 2550 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2551 """ 2552 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2553 2554 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2555 Warning! Broker server used ISO UTC time by default. 2556 2557 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2558 Also, `historyFile` used to update history with `onlyMissing` parameter. 2559 2560 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2561 2562 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2563 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2564 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2565 `"hour"`, `"day"`. Default: `"hour"`. 2566 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2567 False by default. Warning! History appends only from last candle to current time 2568 with always update last candle! 2569 :param csvSep: separator if csv-file is used, `,` by default. 2570 :param show: if `True` then also prints Pandas DataFrame to the console. 2571 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2572 `["date", "time", "open", "high", "low", "close", "volume"]`. 2573 """ 2574 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2575 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2576 history = None # empty pandas object for history 2577 2578 if interval not in TKS_CANDLE_INTERVALS.keys(): 2579 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2580 raise Exception("Incorrect value") 2581 2582 if not (self.ticker or self.figi): 2583 uLogger.error("Ticker or FIGI must be defined!") 2584 raise Exception("Ticker or FIGI required") 2585 2586 if self.ticker and not self.figi: 2587 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2588 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2589 2590 if self.figi and not self.ticker: 2591 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2592 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2593 2594 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2595 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2596 if interval.lower() != "day": 2597 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2598 2599 delta = dtEnd - dtStart # current UTC time minus last time in file 2600 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2601 2602 # calculate history length in candles: 2603 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2604 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2605 length += 1 # to avoid fraction time 2606 2607 # calculate data blocks count: 2608 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2609 2610 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2611 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2612 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2613 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2614 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2615 2616 tempOld = None # pandas object for old history, if --only-missing key present 2617 lastTime = None # datetime object of last old candle in file 2618 2619 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2620 uLogger.debug("--only-missing key present, add only last missing candles...") 2621 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2622 2623 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2624 2625 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2626 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2627 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2628 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2629 2630 # get last datetime object from last string in file or minus 1 delta if file is empty: 2631 if len(tempOld) > 0: 2632 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2633 2634 else: 2635 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2636 2637 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2638 2639 responseJSONs = [] # raw history blocks of data 2640 2641 blockEnd = dtEnd 2642 for item in range(blocks): 2643 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2644 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2645 2646 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2647 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2648 )) 2649 2650 if blockStart == blockEnd: 2651 uLogger.debug("Skipped this zero-length block...") 2652 2653 else: 2654 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2655 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2656 self.body = str({ 2657 "figi": self.figi, 2658 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2659 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2660 "interval": TKS_CANDLE_INTERVALS[interval][0] 2661 }) 2662 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2663 2664 if "code" in responseJSON.keys(): 2665 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2666 2667 else: 2668 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2669 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2670 2671 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2672 2673 blockEnd = blockStart 2674 2675 printCount = len(responseJSONs) # candles to show in console 2676 if responseJSONs: 2677 tempHistory = pd.DataFrame( 2678 data={ 2679 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2680 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2681 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2682 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2683 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2684 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2685 "volume": [int(item["volume"]) for item in responseJSONs], 2686 }, 2687 index=range(len(responseJSONs)), 2688 columns=["date", "time", "open", "high", "low", "close", "volume"], 2689 ) 2690 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2691 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2692 2693 # append only newest candles to old history if --only-missing key present: 2694 if onlyMissing and tempOld is not None and lastTime is not None: 2695 index = 0 # find start index in tempHistory data: 2696 2697 for i, item in tempHistory.iterrows(): 2698 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2699 2700 if curTime == lastTime: 2701 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2702 index = i 2703 printCount = index + 1 2704 break 2705 2706 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2707 2708 else: 2709 history = tempHistory # if no `--only-missing` key then load full data from server 2710 2711 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2712 2713 if history is not None and not history.empty: 2714 if show: 2715 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2716 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2717 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2718 )) 2719 2720 else: 2721 uLogger.warning("Received an empty candles history!") 2722 2723 if self.historyFile is not None: 2724 if history is not None and not history.empty: 2725 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2726 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2727 2728 else: 2729 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2730 2731 else: 2732 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2733 2734 return history 2735 2736 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2737 """ 2738 Load candles history from csv-file and return Pandas DataFrame object. 2739 2740 See also: `History()` and `ShowHistoryChart()` methods. 2741 2742 :param filePath: path to csv-file to open. 2743 """ 2744 loadedHistory = None # init candles data object 2745 2746 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2747 2748 if os.path.exists(filePath): 2749 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2750 2751 tfStr = self.priceModel.FormattedDelta( 2752 self.priceModel.timeframe, 2753 "{days} days {hours}h {minutes}m {seconds}s", 2754 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2755 self.priceModel.timeframe, 2756 "{hours}h {minutes}m {seconds}s", 2757 ) 2758 2759 if loadedHistory is not None and not loadedHistory.empty: 2760 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2761 len(loadedHistory), 2762 tfStr, 2763 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2764 ) 2765 2766 else: 2767 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2768 2769 else: 2770 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2771 2772 return loadedHistory 2773 2774 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2775 """ 2776 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2777 2778 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2779 Default: `index.html` (both for interact and non-interact candlesticks chart). 2780 2781 See also: `History()` and `LoadHistory()` methods. 2782 2783 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2784 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2785 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2786 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2787 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2788 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2789 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2790 """ 2791 if isinstance(candles, str): 2792 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2793 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2794 2795 elif isinstance(candles, pd.DataFrame): 2796 self.priceModel.prices = candles # set candles chain from variable 2797 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2798 2799 if "datetime" not in candles.columns: 2800 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2801 2802 else: 2803 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2804 raise Exception("Incorrect value") 2805 2806 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2807 2808 if interact: 2809 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2810 2811 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2812 2813 else: 2814 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2815 2816 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2817 2818 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2819 2820 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2821 """ 2822 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2823 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2824 2825 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2826 2827 :param operation: string "Buy" or "Sell". 2828 :param lots: volume, integer count of lots >= 1. 2829 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2830 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2831 :param expDate: string "Undefined" by default or local date in future, 2832 it is a string with format `%Y-%m-%d %H:%M:%S`. 2833 :return: JSON with response from broker server. 2834 """ 2835 if self.accountId is None or not self.accountId: 2836 uLogger.error("Variable `accountId` must be defined for using this method!") 2837 raise Exception("Account ID required") 2838 2839 if operation is None or not operation or operation not in ("Buy", "Sell"): 2840 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2841 raise Exception("Incorrect value") 2842 2843 if lots is None or lots < 1: 2844 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2845 lots = 1 2846 2847 if tp is None or tp < 0: 2848 tp = 0 2849 2850 if sl is None or sl < 0: 2851 sl = 0 2852 2853 if expDate is None or not expDate: 2854 expDate = "Undefined" 2855 2856 if not (self.ticker or self.figi): 2857 uLogger.error("Ticker or FIGI must be defined!") 2858 raise Exception("Ticker or FIGI required") 2859 2860 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 2861 self.ticker = instrument["ticker"] 2862 self.figi = instrument["figi"] 2863 2864 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2865 2866 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2867 self.body = str({ 2868 "figi": self.figi, 2869 "quantity": str(lots), 2870 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2871 "accountId": str(self.accountId), 2872 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2873 }) 2874 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2875 2876 if "orderId" in response.keys(): 2877 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2878 operation, response["orderId"], 2879 self.ticker, self.figi, lots, 2880 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2881 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2882 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2883 )) 2884 2885 if tp > 0: 2886 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2887 2888 if sl > 0: 2889 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2890 2891 else: 2892 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.") 2893 2894 return response 2895 2896 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2897 """ 2898 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2899 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2900 2901 See also: `Order()` and `Trade()` docstrings. 2902 2903 :param lots: volume, integer count of lots >= 1. 2904 :param tp: float > 0, take profit price of stop-order. 2905 :param sl: float > 0, stop loss price of stop-order. 2906 :param expDate: it's a local date in future. 2907 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2908 :return: JSON with response from broker server. 2909 """ 2910 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 2911 2912 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2913 """ 2914 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2915 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2916 2917 See also: `Order()` and `Trade()` docstrings. 2918 2919 :param lots: volume, integer count of lots >= 1. 2920 :param tp: float > 0, take profit price of stop-order. 2921 :param sl: float > 0, stop loss price of stop-order. 2922 :param expDate: it's a local date in the future. 2923 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2924 :return: JSON with response from broker server. 2925 """ 2926 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 2927 2928 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 2929 """ 2930 Close position of given instruments. 2931 2932 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 2933 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 2934 This avoids unnecessary downloading data from the server. 2935 """ 2936 if instruments is None or not instruments: 2937 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 2938 raise Exception("Ticker or FIGI required") 2939 2940 if isinstance(instruments, str): 2941 instruments = [instruments] 2942 2943 uniqueInstruments = self.GetUniqueFIGIs(instruments) 2944 if uniqueInstruments: 2945 if portfolio is None or not portfolio: 2946 portfolio = self.Overview(show=False) 2947 2948 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 2949 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 2950 2951 for self.figi in uniqueInstruments: 2952 if self.figi not in allOpened: 2953 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi)) 2954 continue 2955 2956 # search open trade info about instrument by ticker: 2957 instrument = {} 2958 for iType in TKS_INSTRUMENTS: 2959 if instrument: 2960 break 2961 2962 for item in portfolio["stat"][iType]: 2963 if item["figi"] == self.figi: 2964 instrument = item 2965 break 2966 2967 if instrument: 2968 self.ticker = instrument["ticker"] 2969 self.figi = instrument["figi"] 2970 2971 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 2972 self.ticker, 2973 self.figi, 2974 int(instrument["volume"]), 2975 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 2976 )) 2977 2978 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 2979 2980 if tradeLots > 0: 2981 if instrument["blocked"] > 0: 2982 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 2983 instrument["blocked"], 2984 self.ticker, 2985 tradeLots, 2986 )) 2987 2988 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 2989 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 2990 2991 else: 2992 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 2993 2994 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 2995 """ 2996 Close all positions of given instruments with defined type. 2997 2998 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 2999 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3000 This avoids unnecessary downloading data from the server. 3001 """ 3002 if iType not in TKS_INSTRUMENTS: 3003 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3004 3005 else: 3006 if portfolio is None or not portfolio: 3007 portfolio = self.Overview(show=False) 3008 3009 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3010 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3011 3012 if tickers and portfolio: 3013 self.CloseTrades(tickers, portfolio) 3014 3015 else: 3016 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3017 3018 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3019 """ 3020 Universal method to create market or limit orders with all available parameters for current `accountId`. 3021 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3022 3023 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3024 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3025 3026 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3027 then broker immediately open market order as you can do simple --buy or --sell operations! 3028 3029 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3030 When current price will go up or down to target price value then broker opens a limit order. 3031 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3032 3033 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3034 3035 :param operation: string "Buy" or "Sell". 3036 :param orderType: string "Limit" or "Stop". 3037 :param lots: volume, integer count of lots >= 1. 3038 :param targetPrice: target price > 0. This is open trade price for limit order. 3039 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3040 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3041 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3042 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3043 Stop loss order always executed by market price. 3044 :param expDate: string "Undefined" by default or local date in future. 3045 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3046 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3047 A limit order has no expiration date, it lasts until the end of the trading day. 3048 :return: JSON with response from broker server. 3049 """ 3050 if self.accountId is None or not self.accountId: 3051 uLogger.error("Variable `accountId` must be defined for using this method!") 3052 raise Exception("Account ID required") 3053 3054 if operation is None or not operation or operation not in ("Buy", "Sell"): 3055 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3056 raise Exception("Incorrect value") 3057 3058 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3059 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3060 raise Exception("Incorrect value") 3061 3062 if lots is None or lots < 1: 3063 uLogger.error("You must define trade volume > 0: integer count of lots!") 3064 raise Exception("Incorrect value") 3065 3066 if targetPrice is None or targetPrice <= 0: 3067 uLogger.error("Target price for limit-order must be greater than 0!") 3068 raise Exception("Incorrect value") 3069 3070 if limitPrice is None or limitPrice <= 0: 3071 limitPrice = targetPrice 3072 3073 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3074 stopType = "Limit" 3075 3076 if expDate is None or not expDate: 3077 expDate = "Undefined" 3078 3079 if not (self.ticker or self.figi): 3080 uLogger.error("Tocker or FIGI must be defined!") 3081 raise Exception("Ticker or FIGI required") 3082 3083 response = {} 3084 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 3085 self.ticker = instrument["ticker"] 3086 self.figi = instrument["figi"] 3087 3088 if orderType == "Limit": 3089 uLogger.debug( 3090 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3091 self.ticker, self.figi, 3092 operation, lots, targetPrice, instrument["currency"], 3093 )) 3094 3095 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3096 self.body = str({ 3097 "figi": self.figi, 3098 "quantity": str(lots), 3099 "price": FloatToNano(targetPrice), 3100 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3101 "accountId": str(self.accountId), 3102 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3103 }) 3104 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3105 3106 if "orderId" in response.keys(): 3107 uLogger.info( 3108 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3109 response["orderId"], 3110 self.ticker, self.figi, 3111 operation, lots, targetPrice, instrument["currency"], 3112 )) 3113 3114 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3115 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3116 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3117 targetPrice, instrument["currency"], 3118 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3119 )) 3120 3121 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3122 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3123 targetPrice, instrument["currency"], 3124 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3125 )) 3126 3127 else: 3128 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3129 3130 if orderType == "Stop": 3131 uLogger.debug( 3132 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3133 self.ticker, self.figi, 3134 operation, lots, 3135 targetPrice, instrument["currency"], 3136 limitPrice, instrument["currency"], 3137 stopType, expDate, 3138 )) 3139 3140 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3141 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3142 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3143 3144 body = { 3145 "figi": self.figi, 3146 "quantity": str(lots), 3147 "price": FloatToNano(limitPrice), 3148 "stopPrice": FloatToNano(targetPrice), 3149 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3150 "accountId": str(self.accountId), 3151 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3152 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3153 } 3154 3155 if expDateUTC: 3156 body["expireDate"] = expDateUTC 3157 3158 self.body = str(body) 3159 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3160 3161 if "stopOrderId" in response.keys(): 3162 uLogger.info( 3163 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3164 response["stopOrderId"], 3165 self.ticker, self.figi, 3166 operation, lots, 3167 targetPrice, instrument["currency"], 3168 limitPrice, instrument["currency"], 3169 TKS_STOP_ORDER_TYPES[stopOrderType], 3170 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3171 )) 3172 3173 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3174 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3175 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3176 targetPrice, instrument["currency"], 3177 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3178 )) 3179 3180 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3181 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3182 targetPrice, instrument["currency"], 3183 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3184 )) 3185 3186 else: 3187 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3188 3189 return response 3190 3191 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3192 """ 3193 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3194 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3195 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3196 See also: `Order()` docstring. 3197 3198 :param lots: volume, integer count of lots >= 1. 3199 :param targetPrice: target price > 0. This is open trade price for limit order. 3200 :return: JSON with response from broker server. 3201 """ 3202 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3203 3204 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3205 """ 3206 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3207 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3208 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3209 target price value then broker opens a limit order. See also: `Order()` docstring. 3210 3211 :param lots: volume, integer count of lots >= 1. 3212 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3213 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3214 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3215 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3216 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3217 :param expDate: string "Undefined" by default or local date in future. 3218 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3219 This date is converting to UTC format for server. 3220 :return: JSON with response from broker server. 3221 """ 3222 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3223 3224 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3225 """ 3226 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3227 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3228 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3229 See also: `Order()` docstring. 3230 3231 :param lots: volume, integer count of lots >= 1. 3232 :param targetPrice: target price > 0. This is open trade price for limit order. 3233 :return: JSON with response from broker server. 3234 """ 3235 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3236 3237 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3238 """ 3239 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3240 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3241 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3242 target price value then broker opens a limit order. See also: `Order()` docstring. 3243 3244 :param lots: volume, integer count of lots >= 1. 3245 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3246 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3247 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3248 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3249 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3250 :param expDate: string "Undefined" by default or local date in future. 3251 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3252 This date is converting to UTC format for server. 3253 :return: JSON with response from broker server. 3254 """ 3255 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3256 3257 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3258 """ 3259 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3260 3261 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3262 :param allOrdersIDs: pre-received lists of all active pending orders. 3263 This avoids unnecessary downloading data from the server. 3264 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3265 """ 3266 if self.accountId is None or not self.accountId: 3267 uLogger.error("Variable `accountId` must be defined for using this method!") 3268 raise Exception("Account ID required") 3269 3270 if orderIDs: 3271 if allOrdersIDs is None or not allOrdersIDs: 3272 rawOrders = self.RequestPendingOrders() 3273 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3274 3275 if allStopOrdersIDs is None or not allStopOrdersIDs: 3276 rawStopOrders = self.RequestStopOrders() 3277 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3278 3279 for orderID in orderIDs: 3280 idInPendingOrders = orderID in allOrdersIDs 3281 idInStopOrders = orderID in allStopOrdersIDs 3282 3283 if not (idInPendingOrders or idInStopOrders): 3284 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3285 continue 3286 3287 else: 3288 if idInPendingOrders: 3289 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3290 3291 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3292 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3293 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3294 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3295 3296 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3297 if self.moreDebug: 3298 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3299 3300 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3301 3302 else: 3303 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3304 3305 elif idInStopOrders: 3306 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3307 3308 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3309 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3310 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3311 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3312 3313 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3314 if self.moreDebug: 3315 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3316 3317 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3318 3319 else: 3320 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3321 3322 else: 3323 continue 3324 3325 def CloseAllOrders(self) -> None: 3326 """ 3327 Gets a list of open pending and stop orders and cancel it all. 3328 """ 3329 rawOrders = self.RequestPendingOrders() 3330 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3331 lenOrders = len(allOrdersIDs) 3332 3333 rawStopOrders = self.RequestStopOrders() 3334 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3335 lenSOrders = len(allStopOrdersIDs) 3336 3337 if lenOrders > 0 or lenSOrders > 0: 3338 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3339 3340 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3341 3342 else: 3343 uLogger.info("Orders not found, nothing to cancel.") 3344 3345 def CloseAll(self, *args) -> None: 3346 """ 3347 Close all available (not blocked) opened trades and orders. 3348 3349 Also, you can select one or more keywords case-insensitive: 3350 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3351 3352 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3353 """ 3354 overview = self.Overview(show=False) # get all open trades info 3355 3356 if len(args) == 0: 3357 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3358 self.CloseAllOrders() # close all pending and stop orders 3359 3360 for iType in TKS_INSTRUMENTS: 3361 if iType != "Currencies": 3362 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3363 3364 else: 3365 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3366 lowerArgs = [x.lower() for x in args] 3367 3368 if "orders" in lowerArgs: 3369 self.CloseAllOrders() # close all pending and stop orders 3370 3371 for iType in TKS_INSTRUMENTS: 3372 if iType.lower() in lowerArgs and iType != "Currencies": 3373 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3374 3375 @staticmethod 3376 def ParseOrderParameters(operation, **inputParameters): 3377 """ 3378 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3379 3380 :param operation: string "Buy" or "Sell". 3381 :param inputParameters: this is dict of strings that looks like this 3382 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3383 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3384 "prices" key: one or more prices to open limit-orders 3385 Counts of values in lots and prices lists must be equals! 3386 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3387 """ 3388 # TODO: update order grid work with api v2 3389 pass 3390 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3391 # 3392 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3393 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3394 # raise Exception("Incorrect value") 3395 # 3396 # if "l" in inputParameters.keys(): 3397 # inputParameters["lots"] = inputParameters.pop("l") 3398 # 3399 # if "p" in inputParameters.keys(): 3400 # inputParameters["prices"] = inputParameters.pop("p") 3401 # 3402 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3403 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3404 # raise Exception("Incorrect value") 3405 # 3406 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3407 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3408 # 3409 # if len(lots) != len(prices): 3410 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3411 # raise Exception("Incorrect value") 3412 # 3413 # uLogger.debug("Extracted parameters for orders:") 3414 # uLogger.debug("lots = {}".format(lots)) 3415 # uLogger.debug("prices = {}".format(prices)) 3416 # 3417 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3418 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3419 # uLogger.debug("Order parameters: {}".format(result)) 3420 # 3421 # return result 3422 3423 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3424 """ 3425 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3426 3427 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3428 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3429 """ 3430 result = False 3431 msg = "Instrument not defined!" 3432 3433 if portfolio is None or not portfolio: 3434 portfolio = self.Overview(show=False) 3435 3436 if self.ticker: 3437 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3438 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3439 3440 for iType in TKS_INSTRUMENTS: 3441 for instrument in portfolio["stat"][iType]: 3442 if instrument["ticker"] == self.ticker: 3443 result = True 3444 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3445 break 3446 3447 elif self.figi: 3448 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3449 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3450 3451 for iType in TKS_INSTRUMENTS: 3452 for instrument in portfolio["stat"][iType]: 3453 if instrument["figi"] == self.figi: 3454 result = True 3455 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3456 break 3457 3458 else: 3459 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3460 3461 uLogger.debug(msg) 3462 3463 return result 3464 3465 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3466 """ 3467 Returns instrument from the user's portfolio if it presents there. 3468 Instrument must be defined by `ticker` (highly priority) or `figi`. 3469 3470 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3471 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3472 """ 3473 result = None 3474 msg = "Instrument not defined!" 3475 3476 if portfolio is None or not portfolio: 3477 portfolio = self.Overview(show=False) 3478 3479 if self.ticker: 3480 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker)) 3481 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3482 3483 for iType in TKS_INSTRUMENTS: 3484 for instrument in portfolio["stat"][iType]: 3485 if instrument["ticker"] == self.ticker: 3486 result = instrument 3487 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3488 break 3489 3490 elif self.figi: 3491 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3492 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3493 3494 for iType in TKS_INSTRUMENTS: 3495 for instrument in portfolio["stat"][iType]: 3496 if instrument["figi"] == self.figi: 3497 result = instrument 3498 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3499 break 3500 3501 else: 3502 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3503 3504 uLogger.debug(msg) 3505 3506 return result 3507 3508 def RequestLimits(self) -> dict: 3509 """ 3510 Method for obtaining the available funds for withdrawal for current `accountId`. 3511 3512 See also: 3513 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3514 - `OverviewLimits()` method 3515 3516 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3517 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3518 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3519 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3520 """ 3521 if self.accountId is None or not self.accountId: 3522 uLogger.error("Variable `accountId` must be defined for using this method!") 3523 raise Exception("Account ID required") 3524 3525 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3526 3527 self.body = str({"accountId": self.accountId}) 3528 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3529 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3530 3531 if self.moreDebug: 3532 uLogger.debug("Records about available funds for withdrawal successfully received") 3533 3534 return rawLimits 3535 3536 def OverviewLimits(self, show: bool = False) -> dict: 3537 """ 3538 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3539 3540 See also: `RequestLimits()`. 3541 3542 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3543 :return: dict with raw parsed data from server and some calculated statistics about it. 3544 """ 3545 if self.accountId is None or not self.accountId: 3546 uLogger.error("Variable `accountId` must be defined for using this method!") 3547 raise Exception("Account ID required") 3548 3549 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3550 3551 view = { 3552 "rawLimits": rawLimits, 3553 "limits": { # parsed data for every currency: 3554 "money": { # this is an array of portfolio currency positions 3555 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3556 }, 3557 "blocked": { # this is an array of blocked currency 3558 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3559 }, 3560 "blockedGuarantee": { # this is locked money under collateral for futures 3561 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3562 }, 3563 }, 3564 } 3565 3566 # --- Prepare text table with limits in human-readable format: 3567 if show: 3568 info = [ 3569 "# Withdrawal limits\n\n", 3570 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3571 "* **Account ID:** [{}]\n".format(self.accountId), 3572 ] 3573 3574 if view["limits"]["money"]: 3575 info.extend([ 3576 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3577 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3578 ]) 3579 3580 else: 3581 info.append("\nNo withdrawal limits\n") 3582 3583 for curr in view["limits"]["money"].keys(): 3584 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3585 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3586 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3587 3588 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3589 "[{}]".format(curr), 3590 "{:.2f}".format(view["limits"]["money"][curr]), 3591 "{:.2f}".format(availableMoney), 3592 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3593 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3594 ) 3595 3596 if curr == "rub": 3597 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3598 3599 else: 3600 info.append(infoStr) 3601 3602 infoText = "".join(info) 3603 3604 uLogger.info(infoText) 3605 3606 if self.withdrawalLimitsFile: 3607 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3608 fH.write(infoText) 3609 3610 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3611 3612 return view 3613 3614 def RequestAccounts(self) -> dict: 3615 """ 3616 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3617 3618 See also: 3619 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3620 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3621 - `OverviewUserInfo()` method 3622 3623 :return: dict with raw data from server that contains accounts info. Example of dict: 3624 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3625 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3626 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3627 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3628 """ 3629 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3630 3631 self.body = str({}) 3632 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3633 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3634 3635 if self.moreDebug: 3636 uLogger.debug("Records about available accounts successfully received") 3637 3638 return rawAccounts 3639 3640 def RequestUserInfo(self) -> dict: 3641 """ 3642 Method for requesting common user's information. 3643 3644 See also: 3645 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3646 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3647 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3648 - `OverviewUserInfo()` method 3649 3650 :return: dict with raw data from server that contains user's information. Example of dict: 3651 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3652 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3653 """ 3654 uLogger.debug("Requesting common user's information. Wait, please...") 3655 3656 self.body = str({}) 3657 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3658 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3659 3660 if self.moreDebug: 3661 uLogger.debug("Records about current user successfully received") 3662 3663 return rawUserInfo 3664 3665 def RequestMarginStatus(self, accountId: str = None) -> dict: 3666 """ 3667 Method for requesting margin calculation for defined account ID. 3668 3669 See also: 3670 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3671 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3672 - `OverviewUserInfo()` method 3673 3674 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3675 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3676 Example of responses: 3677 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3678 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3679 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3680 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3681 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3682 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3683 """ 3684 if accountId is None or not accountId: 3685 if self.accountId is None or not self.accountId: 3686 uLogger.error("Variable `accountId` must be defined for using this method!") 3687 raise Exception("Account ID required") 3688 3689 else: 3690 accountId = self.accountId # use `self.accountId` (main ID) by default 3691 3692 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3693 3694 self.body = str({"accountId": accountId}) 3695 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3696 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3697 3698 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3699 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3700 rawMargin = {} 3701 3702 else: 3703 if self.moreDebug: 3704 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3705 3706 return rawMargin 3707 3708 def RequestTariffLimits(self) -> dict: 3709 """ 3710 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3711 3712 See also: 3713 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3714 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3715 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3716 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3717 - `OverviewUserInfo()` method 3718 3719 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3720 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3721 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3722 """ 3723 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3724 3725 self.body = str({}) 3726 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3727 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3728 3729 if self.moreDebug: 3730 uLogger.debug("Records with limits of current tariff successfully received") 3731 3732 return rawTariffLimits 3733 3734 def RequestBondCoupons(self, iJSON: dict) -> dict: 3735 """ 3736 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3737 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3738 All dates are in UTC timezone. 3739 3740 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3741 Documentation: 3742 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3743 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3744 3745 See also: `ExtendBondsData()`. 3746 3747 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3748 If raw iJSON is not data of bond then server returns an error [400] with message: 3749 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3750 :return: dictionary with bond payment calendar. Response example 3751 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3752 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3753 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3754 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3755 """ 3756 if iJSON["figi"] is None or not iJSON["figi"]: 3757 uLogger.error("FIGI must be defined for using this method!") 3758 raise Exception("FIGI required") 3759 3760 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3761 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3762 3763 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3764 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3765 self.figi, 3766 startDate, 3767 endDate, 3768 )) 3769 3770 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3771 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3772 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 3773 3774 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3775 uLogger.warning("Instrument type is not bond!") 3776 3777 else: 3778 if self.moreDebug: 3779 uLogger.debug("Records about bond payment calendar successfully received") 3780 3781 return calendar 3782 3783 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3784 """ 3785 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3786 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3787 coupon yields, current yields and some statistics etc. 3788 3789 WARNING! This is too long operation if a lot of bonds requested from broker server. 3790 3791 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3792 3793 :param instruments: list of strings with tickers or FIGIs. 3794 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3795 for further used by data scientists or stock analytics. 3796 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3797 In XLSX-file and Pandas DataFrame fields mean: 3798 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3799 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3800 """ 3801 if instruments is None or not instruments: 3802 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3803 raise Exception("Ticker or FIGI required") 3804 3805 if isinstance(instruments, str): 3806 instruments = [instruments] 3807 3808 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3809 3810 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3811 3812 iCount = len(uniqueInstruments) 3813 tooLong = iCount >= 20 3814 if tooLong: 3815 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3816 3817 bonds = None 3818 for i, self.figi in enumerate(uniqueInstruments): 3819 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3820 3821 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3822 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3823 rawBond = self.SearchByFIGI(requestPrice=True) 3824 3825 # Widen raw data with UTC current time (iData["actualDateTime"]): 3826 actualDate = datetime.now(tzutc()) 3827 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3828 3829 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3830 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3831 3832 # Replace some values with human-readable: 3833 iData["nominalCurrency"] = iData["nominal"]["currency"] 3834 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3835 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3836 iData["aciCurrency"] = iData["aciValue"]["currency"] 3837 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3838 iData["issueSize"] = int(iData["issueSize"]) 3839 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3840 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3841 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3842 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3843 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3844 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3845 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3846 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3847 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3848 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3849 3850 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3851 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3852 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3853 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3854 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3855 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3856 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3857 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3858 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3859 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3860 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3861 3862 # Widen raw data with calendar data from `rawCalendar` values: 3863 calendarData = [] 3864 if "events" in iData["rawCalendar"].keys(): 3865 for item in iData["rawCalendar"]["events"]: 3866 calendarData.append({ 3867 "couponDate": item["couponDate"], 3868 "couponNumber": int(item["couponNumber"]), 3869 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3870 "payCurrency": item["payOneBond"]["currency"], 3871 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3872 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3873 "couponStartDate": item["couponStartDate"], 3874 "couponEndDate": item["couponEndDate"], 3875 "couponPeriod": item["couponPeriod"], 3876 }) 3877 3878 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3879 if "maturityDate" not in iData.keys(): 3880 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3881 3882 # Widen raw data with Coupon Rate. 3883 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3884 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3885 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3886 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3887 3888 # Widen raw data with Yield to Maturity (YTM) on current date. 3889 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3890 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3891 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3892 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3893 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3894 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3895 3896 iData["calendar"] = calendarData # adds calendar at the end 3897 3898 # Remove not used data: 3899 iData.pop("uid") 3900 iData.pop("positionUid") 3901 iData.pop("currentPrice") 3902 iData.pop("rawCalendar") 3903 3904 colNames = list(iData.keys()) 3905 if bonds is None: 3906 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3907 3908 else: 3909 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3910 3911 else: 3912 uLogger.warning("Instrument is not a bond!") 3913 3914 processed = round(100 * (i + 1) / iCount, 1) 3915 if tooLong and processed % 5 == 0: 3916 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3917 3918 else: 3919 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3920 3921 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3922 3923 # Saving bonds from Pandas DataFrame to XLSX sheet: 3924 if xlsx and self.bondsXLSXFile: 3925 with pd.ExcelWriter( 3926 path=self.bondsXLSXFile, 3927 date_format=TKS_DATE_FORMAT, 3928 datetime_format=TKS_DATE_TIME_FORMAT, 3929 mode="w", 3930 ) as writer: 3931 bonds.to_excel( 3932 writer, 3933 sheet_name="Extended bonds data", 3934 index=True, 3935 encoding="UTF-8", 3936 freeze_panes=(1, 1), 3937 ) # saving as XLSX-file with freeze first row and column as headers 3938 3939 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 3940 3941 return bonds 3942 3943 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 3944 """ 3945 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 3946 3947 WARNING! This is too long operation if a lot of bonds requested from broker server. 3948 3949 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 3950 3951 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 3952 extended information about bonds: main info, current prices, bond payment calendar, 3953 coupon yields, current yields and some statistics etc. 3954 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 3955 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 3956 for further used by data scientists or stock analytics. 3957 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 3958 """ 3959 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 3960 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 3961 3962 uLogger.debug("Generating bond payments calendar data. Wait, please...") 3963 3964 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 3965 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 3966 calendar = None 3967 for bond in extBonds.iterrows(): 3968 for item in bond[1]["calendar"]: 3969 cData = { 3970 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 3971 "couponDate": item["couponDate"], 3972 "figi": bond[1]["figi"], 3973 "ticker": bond[1]["ticker"], 3974 "name": bond[1]["name"], 3975 "couponNumber": item["couponNumber"], 3976 "payOneBond": item["payOneBond"], 3977 "payCurrency": item["payCurrency"], 3978 "couponType": item["couponType"], 3979 "couponPeriod": item["couponPeriod"], 3980 "fixDate": item["fixDate"], 3981 "couponStartDate": item["couponStartDate"], 3982 "couponEndDate": item["couponEndDate"], 3983 } 3984 3985 if calendar is None: 3986 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 3987 3988 else: 3989 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 3990 3991 if calendar is not None: 3992 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 3993 3994 # Saving calendar from Pandas DataFrame to XLSX sheet: 3995 if xlsx: 3996 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 3997 3998 with pd.ExcelWriter( 3999 path=xlsxCalendarFile, 4000 date_format=TKS_DATE_FORMAT, 4001 datetime_format=TKS_DATE_TIME_FORMAT, 4002 mode="w", 4003 ) as writer: 4004 humanReadable = calendar.copy(deep=True) 4005 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4006 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4007 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4008 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4009 humanReadable.columns = colNames # human-readable column names 4010 4011 humanReadable.to_excel( 4012 writer, 4013 sheet_name="Bond payments calendar", 4014 index=False, 4015 encoding="UTF-8", 4016 freeze_panes=(1, 2), 4017 ) # saving as XLSX-file with freeze first row and column as headers 4018 4019 del humanReadable # release df in memory 4020 4021 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4022 4023 return calendar 4024 4025 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4026 """ 4027 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4028 Also, creates Markdown file with calendar data, `calendar.md` by default. 4029 4030 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4031 4032 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4033 extended information about bonds: main info, current prices, bond payment calendar, 4034 coupon yields, current yields and some statistics etc. 4035 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4036 :param show: if `True` then also printing bonds payment calendar to the console, 4037 otherwise save to file `calendarFile` only. `False` by default. 4038 :return: multilines text in Markdown format with bonds payment calendar as a table. 4039 """ 4040 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4041 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4042 4043 infoText = "# Bond payments calendar\n\n" 4044 4045 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4046 4047 if not (calendar is None or calendar.empty): 4048 splitLine = "| | | | | | | | | |\n" 4049 4050 info = [ 4051 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4052 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4053 ] 4054 4055 newMonth = False 4056 notOneBond = calendar["figi"].nunique() > 1 4057 for i, bond in enumerate(calendar.iterrows()): 4058 if newMonth and notOneBond: 4059 info.append(splitLine) 4060 4061 info.append( 4062 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4063 " √" if bond[1]["paid"] else " —", 4064 bond[1]["couponDate"].split("T")[0], 4065 bond[1]["figi"], 4066 bond[1]["ticker"], 4067 bond[1]["couponNumber"], 4068 "{} {}".format( 4069 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4070 bond[1]["payCurrency"], 4071 ), 4072 bond[1]["couponType"], 4073 bond[1]["couponPeriod"], 4074 bond[1]["fixDate"].split("T")[0], 4075 ) 4076 ) 4077 4078 if i < len(calendar.values) - 1: 4079 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4080 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4081 newMonth = False if curDate.month == nextDate.month else True 4082 4083 else: 4084 newMonth = False 4085 4086 infoText += "".join(info) 4087 4088 if show: 4089 uLogger.info("{}".format(infoText)) 4090 4091 if self.calendarFile is not None: 4092 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4093 fH.write(infoText) 4094 4095 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4096 4097 else: 4098 infoText += "No data\n" 4099 4100 return infoText 4101 4102 def OverviewAccounts(self, show: bool = False) -> dict: 4103 """ 4104 Method for parsing and show simple table with all available user accounts. 4105 4106 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4107 4108 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4109 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4110 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4111 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4112 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4113 "closed": "—", "access": "Full access" }, ...}}` 4114 """ 4115 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4116 4117 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4118 accounts = { 4119 item["id"]: { 4120 "type": TKS_ACCOUNT_TYPES[item["type"]], 4121 "name": item["name"], 4122 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4123 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4124 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4125 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4126 } for item in rawAccounts["accounts"] 4127 } 4128 4129 # Raw and parsed data with some fields replaced in "stat" section: 4130 view = { 4131 "rawAccounts": rawAccounts, 4132 "stat": accounts, 4133 } 4134 4135 # --- Prepare simple text table with only accounts data in human-readable format: 4136 if show: 4137 info = [ 4138 "# User accounts\n\n", 4139 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4140 "| Account ID | Type | Status | Name |\n", 4141 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4142 ] 4143 4144 for account in view["stat"].keys(): 4145 info.extend([ 4146 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4147 account, 4148 view["stat"][account]["type"], 4149 view["stat"][account]["status"], 4150 view["stat"][account]["name"], 4151 ) 4152 ]) 4153 4154 infoText = "".join(info) 4155 4156 uLogger.info(infoText) 4157 4158 if self.userAccountsFile: 4159 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4160 fH.write(infoText) 4161 4162 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4163 4164 return view 4165 4166 def OverviewUserInfo(self, show: bool = False) -> dict: 4167 """ 4168 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4169 4170 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4171 4172 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4173 :return: dict with raw parsed data from server and some calculated statistics about it. 4174 """ 4175 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4176 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4177 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4178 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4179 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4180 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4181 4182 # This is dict with parsed common user data: 4183 userInfo = { 4184 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4185 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4186 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4187 "tariff": rawUserInfo["tariff"], 4188 } 4189 4190 # This is an array of dict with parsed margin statuses for every account IDs: 4191 margins = {} 4192 for accountId in accounts.keys(): 4193 if rawMargins[accountId]: 4194 margins[accountId] = { 4195 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4196 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4197 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4198 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4199 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4200 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4201 } 4202 4203 else: 4204 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4205 4206 unary = {} # unary-connection limits 4207 for item in rawTariffLimits["unaryLimits"]: 4208 if item["limitPerMinute"] in unary.keys(): 4209 unary[item["limitPerMinute"]].extend(item["methods"]) 4210 4211 else: 4212 unary[item["limitPerMinute"]] = item["methods"] 4213 4214 stream = {} # stream-connection limits 4215 for item in rawTariffLimits["streamLimits"]: 4216 if item["limit"] in stream.keys(): 4217 stream[item["limit"]].extend(item["streams"]) 4218 4219 else: 4220 stream[item["limit"]] = item["streams"] 4221 4222 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4223 limits = { 4224 "unary": unary, 4225 "stream": stream, 4226 } 4227 4228 # Raw and parsed data as an output result: 4229 view = { 4230 "rawUserInfo": rawUserInfo, 4231 "rawAccounts": rawAccounts, 4232 "rawMargins": rawMargins, 4233 "rawTariffLimits": rawTariffLimits, 4234 "stat": { 4235 "userInfo": userInfo, 4236 "accounts": accounts, 4237 "margins": margins, 4238 "limits": limits, 4239 }, 4240 } 4241 4242 # --- Prepare text table with user information in human-readable format: 4243 if show: 4244 info = [ 4245 "# Full user information\n\n", 4246 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4247 "## Common information\n\n", 4248 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4249 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4250 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4251 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4252 "\n## User accounts\n\n", 4253 ] 4254 4255 for account in view["stat"]["accounts"].keys(): 4256 info.extend([ 4257 "### ID: [{}]\n\n".format(account), 4258 "| Parameters | Values |\n", 4259 "|----------------------|--------------------------------------------------------------|\n", 4260 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4261 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4262 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4263 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4264 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4265 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4266 ]) 4267 4268 if margins[account]: 4269 info.extend([ 4270 "| Margin status: | Enabled |\n", 4271 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4272 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4273 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4274 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4275 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4276 ]) 4277 4278 else: 4279 info.append("| Margin status: | Disabled |\n\n") 4280 4281 info.extend([ 4282 "\n## Current user tariff limits\n", 4283 "\nSee also:\n", 4284 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4285 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4286 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4287 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4288 "\n### Unary limits\n", 4289 ]) 4290 4291 if unary: 4292 for key, values in sorted(unary.items()): 4293 info.append("\n* Max requests per minute: {}\n".format(key)) 4294 4295 for value in values: 4296 info.append(" - {}\n".format(value)) 4297 4298 else: 4299 info.append("\nNot available\n") 4300 4301 info.append("\n### Stream limits\n") 4302 4303 if stream: 4304 for key, values in sorted(stream.items()): 4305 info.append("\n* Max stream connections: {}\n".format(key)) 4306 4307 for value in values: 4308 info.append(" - {}\n".format(value)) 4309 4310 else: 4311 info.append("\nNot available\n") 4312 4313 infoText = "".join(info) 4314 4315 uLogger.info(infoText) 4316 4317 if self.userInfoFile: 4318 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4319 fH.write(infoText) 4320 4321 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4322 4323 return view 4324 4325 4326class Args: 4327 """ 4328 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4329 """ 4330 def __init__(self, **kwargs): 4331 self.__dict__.update(kwargs) 4332 4333 def __getattr__(self, item): 4334 return None 4335 4336 4337def ParseArgs(): 4338 """This function get and parse command line keys.""" 4339 parser = ArgumentParser() # command-line string parser 4340 4341 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4342 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4343 4344 # --- options: 4345 4346 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4347 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4348 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4349 4350 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4351 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4352 4353 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4354 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4355 4356 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4357 4358 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4359 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4360 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4361 4362 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4363 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4364 4365 # --- commands: 4366 4367 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4368 4369 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4370 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4371 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4372 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4373 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4374 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4375 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4376 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4377 4378 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4379 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4380 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4381 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4382 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4383 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4384 4385 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4386 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4387 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4388 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4389 4390 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4391 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4392 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4393 4394 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4395 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4396 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4397 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4398 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4399 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4400 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4401 4402 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4403 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4404 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4405 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4406 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4407 4408 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4409 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4410 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4411 4412 cmdArgs = parser.parse_args() 4413 return cmdArgs 4414 4415 4416def Main(**kwargs): 4417 """ 4418 Main function for work with TKSBrokerAPI in the console. 4419 4420 See examples: 4421 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4422 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4423 """ 4424 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4425 4426 if args.debug_level: 4427 uLogger.level = 10 # always debug level by default 4428 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4429 4430 exitCode = 0 4431 start = datetime.now(tzutc()) 4432 uLogger.debug("=-" * 50) 4433 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4434 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4435 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4436 )) 4437 4438 # trying to calculate full current version: 4439 buildVersion = __version__ 4440 try: 4441 v = version("tksbrokerapi") 4442 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4443 4444 except Exception: 4445 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4446 4447 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4448 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4449 4450 try: 4451 if args.version: 4452 print("TKSBrokerAPI {}".format(buildVersion)) 4453 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4454 4455 else: 4456 # Init class for trading with Tinkoff Broker: 4457 trader = TinkoffBrokerServer( 4458 token=args.token, 4459 accountId=args.account_id, 4460 useCache=not args.no_cache, 4461 ) 4462 4463 # --- set some options: 4464 4465 if args.more: 4466 trader.moreDebug = True 4467 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4468 4469 if args.ticker: 4470 ticker = args.ticker.upper() # Tickers may be upper case only 4471 4472 if ticker in trader.aliasesKeys: 4473 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4474 4475 else: 4476 trader.ticker = ticker 4477 4478 if args.figi: 4479 trader.figi = args.figi.upper() # FIGIs may be upper case only 4480 4481 if args.depth is not None: 4482 trader.depth = args.depth 4483 4484 # --- do one command: 4485 4486 if args.list: 4487 if args.output is not None: 4488 trader.instrumentsFile = args.output 4489 4490 trader.ShowInstrumentsInfo(show=True) 4491 4492 elif args.list_xlsx: 4493 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4494 4495 elif args.bonds_xlsx is not None: 4496 if args.output is not None: 4497 trader.bondsXLSXFile = args.output 4498 4499 if len(args.bonds_xlsx) == 0: 4500 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4501 4502 else: 4503 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4504 4505 elif args.search: 4506 if args.output is not None: 4507 trader.searchResultsFile = args.output 4508 4509 trader.SearchInstruments(pattern=args.search[0], show=True) 4510 4511 elif args.info: 4512 if not (args.ticker or args.figi): 4513 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4514 raise Exception("Ticker or FIGI required") 4515 4516 if args.output is not None: 4517 trader.infoFile = args.output 4518 4519 if args.ticker: 4520 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4521 4522 else: 4523 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4524 4525 elif args.calendar is not None: 4526 if args.output is not None: 4527 trader.calendarFile = args.output 4528 4529 if len(args.calendar) == 0: 4530 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4531 4532 else: 4533 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4534 4535 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4536 4537 elif args.price: 4538 if not (args.ticker or args.figi): 4539 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4540 raise Exception("Ticker or FIGI required") 4541 4542 trader.GetCurrentPrices(show=True) 4543 4544 elif args.prices is not None: 4545 if args.output is not None: 4546 trader.pricesFile = args.output 4547 4548 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4549 4550 elif args.overview: 4551 if args.output is not None: 4552 trader.overviewFile = args.output 4553 4554 trader.Overview(show=True, details="full") 4555 4556 elif args.overview_digest: 4557 if args.output is not None: 4558 trader.overviewDigestFile = args.output 4559 4560 trader.Overview(show=True, details="digest") 4561 4562 elif args.overview_positions: 4563 if args.output is not None: 4564 trader.overviewPositionsFile = args.output 4565 4566 trader.Overview(show=True, details="positions") 4567 4568 elif args.overview_orders: 4569 if args.output is not None: 4570 trader.overviewOrdersFile = args.output 4571 4572 trader.Overview(show=True, details="orders") 4573 4574 elif args.overview_analytics: 4575 if args.output is not None: 4576 trader.overviewAnalyticsFile = args.output 4577 4578 trader.Overview(show=True, details="analytics") 4579 4580 elif args.overview_calendar: 4581 if args.output is not None: 4582 trader.overviewAnalyticsFile = args.output 4583 4584 trader.Overview(show=True, details="calendar") 4585 4586 elif args.deals is not None: 4587 if args.output is not None: 4588 trader.reportFile = args.output 4589 4590 if 0 <= len(args.deals) < 3: 4591 trader.Deals( 4592 start=args.deals[0] if len(args.deals) >= 1 else None, 4593 end=args.deals[1] if len(args.deals) == 2 else None, 4594 show=True, # Always show deals report in console 4595 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4596 ) 4597 4598 else: 4599 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4600 raise Exception("Incorrect value") 4601 4602 elif args.history is not None: 4603 if args.output is not None: 4604 trader.historyFile = args.output 4605 4606 if 0 <= len(args.history) < 3: 4607 dataReceived = trader.History( 4608 start=args.history[0] if len(args.history) >= 1 else None, 4609 end=args.history[1] if len(args.history) == 2 else None, 4610 interval="hour" if args.interval is None or not args.interval else args.interval, 4611 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4612 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4613 show=True, # shows all downloaded candles in console 4614 ) 4615 4616 if args.render_chart is not None and dataReceived is not None: 4617 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4618 4619 trader.ShowHistoryChart( 4620 candles=dataReceived, 4621 interact=iChart, 4622 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4623 ) 4624 4625 else: 4626 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4627 raise Exception("Incorrect value") 4628 4629 elif args.load_history is not None: 4630 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4631 4632 if args.render_chart is not None and histData is not None: 4633 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4634 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4635 4636 trader.ShowHistoryChart( 4637 candles=histData, 4638 interact=iChart, 4639 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4640 ) 4641 4642 elif args.trade is not None: 4643 if 1 <= len(args.trade) <= 5: 4644 trader.Trade( 4645 operation=args.trade[0], 4646 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4647 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4648 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4649 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4650 ) 4651 4652 else: 4653 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4654 4655 elif args.buy is not None: 4656 if 0 <= len(args.buy) <= 4: 4657 trader.Buy( 4658 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4659 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4660 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4661 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4662 ) 4663 4664 else: 4665 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4666 4667 elif args.sell is not None: 4668 if 0 <= len(args.sell) <= 4: 4669 trader.Sell( 4670 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4671 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4672 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4673 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4674 ) 4675 4676 else: 4677 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4678 4679 elif args.order: 4680 if 4 <= len(args.order) <= 7: 4681 trader.Order( 4682 operation=args.order[0], 4683 orderType=args.order[1], 4684 lots=int(args.order[2]), 4685 targetPrice=float(args.order[3]), 4686 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4687 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4688 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4689 ) 4690 4691 else: 4692 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4693 4694 elif args.buy_limit: 4695 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4696 4697 elif args.sell_limit: 4698 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4699 4700 elif args.buy_stop: 4701 if 2 <= len(args.buy_stop) <= 7: 4702 trader.BuyStop( 4703 lots=int(args.buy_stop[0]), 4704 targetPrice=float(args.buy_stop[1]), 4705 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4706 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4707 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4708 ) 4709 4710 else: 4711 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4712 4713 elif args.sell_stop: 4714 if 2 <= len(args.sell_stop) <= 7: 4715 trader.SellStop( 4716 lots=int(args.sell_stop[0]), 4717 targetPrice=float(args.sell_stop[1]), 4718 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4719 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4720 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4721 ) 4722 4723 else: 4724 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4725 4726 # elif args.buy_order_grid is not None: 4727 # # update order grid work with api v2 4728 # if len(args.buy_order_grid) == 2: 4729 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4730 # 4731 # for order in orderParams: 4732 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4733 # 4734 # else: 4735 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4736 # 4737 # elif args.sell_order_grid is not None: 4738 # # update order grid work with api v2 4739 # if len(args.sell_order_grid) >= 2: 4740 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4741 # 4742 # for order in orderParams: 4743 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4744 # 4745 # else: 4746 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4747 4748 elif args.close_order is not None: 4749 trader.CloseOrders(args.close_order) # close only one order 4750 4751 elif args.close_orders is not None: 4752 trader.CloseOrders(args.close_orders) # close list of orders 4753 4754 elif args.close_trade: 4755 if not (args.ticker or args.figi): 4756 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4757 raise Exception("Ticker or FIGI required") 4758 4759 if args.ticker: 4760 trader.CloseTrades([args.ticker]) # close only one trade by ticker (priority) 4761 4762 else: 4763 trader.CloseTrades([args.figi]) # close only one trade by FIGI 4764 4765 elif args.close_trades is not None: 4766 trader.CloseTrades(args.close_trades) # close trades for list of tickers 4767 4768 elif args.close_all is not None: 4769 trader.CloseAll(*args.close_all) 4770 4771 elif args.limits: 4772 if args.output is not None: 4773 trader.withdrawalLimitsFile = args.output 4774 4775 trader.OverviewLimits(show=True) 4776 4777 elif args.user_info: 4778 if args.output is not None: 4779 trader.userInfoFile = args.output 4780 4781 trader.OverviewUserInfo(show=True) 4782 4783 elif args.account: 4784 if args.output is not None: 4785 trader.userAccountsFile = args.output 4786 4787 trader.OverviewAccounts(show=True) 4788 4789 else: 4790 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4791 raise Exception("There is no command to execute") 4792 4793 except Exception: 4794 trace = tb.format_exc() 4795 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4796 if e in trace: 4797 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4798 break 4799 4800 uLogger.debug(trace) 4801 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4802 exitCode = 255 # an error occurred, must be open a ticket for this issue 4803 4804 finally: 4805 finish = datetime.now(tzutc()) 4806 4807 if exitCode == 0: 4808 if args.more: 4809 uLogger.debug("All operations were finished success (summary code is 0).") 4810 4811 else: 4812 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4813 os.path.abspath(uLog.defaultLogFile), exitCode, 4814 )) 4815 4816 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4817 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4818 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4819 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4820 )) 4821 uLogger.debug("=-" * 50) 4822 4823 if not kwargs: 4824 sys.exit(exitCode) 4825 4826 else: 4827 return exitCode 4828 4829 4830if __name__ == "__main__": 4831 Main()
77class TinkoffBrokerServer: 78 """ 79 This class implements methods to work with Tinkoff broker server. 80 81 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 82 83 About `token`: https://tinkoff.github.io/investAPI/token/ 84 """ 85 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 86 """ 87 Main class init. 88 89 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 90 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 91 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 92 :param useCache: use default cache file with raw data to use instead of `iList`. 93 True by default. Cache is auto-update if new day has come. 94 If you don't want to use cache and always updates raw data then set `useCache=False`. 95 :param defaultCache: path to default cache file. `dump.json` by default. 96 """ 97 if token is None or not token: 98 try: 99 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 100 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 101 102 except KeyError: 103 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 104 raise Exception("Token required") 105 106 else: 107 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 108 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 109 110 if accountId is None or not accountId: 111 try: 112 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 113 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 114 115 except KeyError: 116 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 117 118 else: 119 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 120 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 121 122 self.version = __version__ # duplicate here used TKSBrokerAPI main version 123 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 124 125 Latest version: https://pypi.org/project/tksbrokerapi/ 126 """ 127 128 self.aliases = TKS_TICKER_ALIASES 129 """Some aliases instead official tickers. 130 131 See also: `TKSEnums.TKS_TICKER_ALIASES` 132 """ 133 134 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 135 136 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 137 138 self.ticker = "" 139 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 140 141 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 142 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 143 144 See also: `SearchByTicker()`, `SearchInstruments()`. 145 """ 146 147 self.figi = "" 148 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 149 150 See also: `SearchByFIGI()`, `SearchInstruments()`. 151 """ 152 153 self.depth = 1 154 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 155 156 See also: `GetCurrentPrices()`. 157 """ 158 159 self.server = r"https://invest-public-api.tinkoff.ru/rest" 160 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 161 162 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 163 """ 164 165 uLogger.debug("Broker API server: {}".format(self.server)) 166 167 self.timeout = 15 168 """Server operations timeout in seconds. Default: `15`. 169 170 See also: `SendAPIRequest()`. 171 """ 172 173 self.headers = { 174 "Content-Type": "application/json", 175 "accept": "application/json", 176 "Authorization": "Bearer {}".format(self.token), 177 "x-app-name": "Tim55667757.TKSBrokerAPI", 178 } 179 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 180 181 See also: `SendAPIRequest()`. 182 """ 183 184 self.body = None 185 """Request body which send to broker server. Default: `None`. 186 187 See also: `SendAPIRequest()`. 188 """ 189 190 self.moreDebug = False 191 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 192 193 self.historyFile = None 194 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 195 196 See also: `History()`. 197 """ 198 199 self.htmlHistoryFile = "index.html" 200 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 201 202 See also: `ShowHistoryChart()`. 203 """ 204 205 self.instrumentsFile = "instruments.md" 206 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 207 208 See also: `ShowInstrumentsInfo()`. 209 """ 210 211 self.searchResultsFile = "search-results.md" 212 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 213 214 See also: `SearchInstruments()`. 215 """ 216 217 self.pricesFile = "prices.md" 218 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 219 220 See also: `GetListOfPrices()`. 221 """ 222 223 self.infoFile = "info.md" 224 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 225 226 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 227 """ 228 229 self.bondsXLSXFile = "ext-bonds.xlsx" 230 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 231 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 232 233 See also: `ExtendBondsData()`. 234 """ 235 236 self.calendarFile = "calendar.md" 237 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 238 239 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 240 241 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 242 """ 243 244 self.overviewFile = "overview.md" 245 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 246 247 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 248 """ 249 250 self.overviewDigestFile = "overview-digest.md" 251 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 252 253 See also: `Overview()` with parameter `details="digest"`. 254 """ 255 256 self.overviewPositionsFile = "overview-positions.md" 257 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 258 259 See also: `Overview()` with parameter `details="positions"`. 260 """ 261 262 self.overviewOrdersFile = "overview-orders.md" 263 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 264 265 See also: `Overview()` with parameter `details="orders"`. 266 """ 267 268 self.overviewAnalyticsFile = "overview-analytics.md" 269 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 270 271 See also: `Overview()` with parameter `details="analytics"`. 272 """ 273 274 self.overviewBondsCalendarFile = "overview-calendar.md" 275 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 276 277 See also: `Overview()` with parameter `details="calendar"`. 278 """ 279 280 self.reportFile = "deals.md" 281 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 282 283 See also: `Deals()`. 284 """ 285 286 self.withdrawalLimitsFile = "limits.md" 287 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 288 289 See also: `OverviewLimits()` and `RequestLimits()`. 290 """ 291 292 self.userInfoFile = "user-info.md" 293 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 294 295 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 296 """ 297 298 self.userAccountsFile = "accounts.md" 299 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 300 301 See also: `OverviewAccounts()`, `RequestAccounts()`. 302 """ 303 304 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 305 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 306 307 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 308 309 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 310 """ 311 312 self.iList = None # init iList for raw instruments data 313 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 314 315 See also: `Listing()`, `DumpInstruments()`. 316 """ 317 318 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 319 if useCache: 320 if os.path.exists(self.iListDumpFile): 321 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 322 curTime = datetime.now(tzutc()) 323 324 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 325 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 326 327 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 328 329 else: 330 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 331 332 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 333 os.path.abspath(self.iListDumpFile), 334 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 335 )) 336 337 else: 338 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 339 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 340 341 else: 342 self.iList = self.Listing() # request new raw instruments data from broker server 343 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 344 345 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 346 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 347 348 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 349 """ 350 351 def _ParseJSON(self, rawData="{}") -> dict: 352 """ 353 Parse JSON from response string. 354 355 :param rawData: this is a string with JSON-formatted text. 356 :return: JSON (dictionary), parsed from server response string. 357 """ 358 responseJSON = json.loads(rawData) if rawData else {} 359 360 if self.moreDebug: 361 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 362 363 return responseJSON 364 365 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 366 """ 367 Send GET or POST request to broker server and receive JSON object. 368 369 self.header: must be defining with dictionary of headers. 370 self.body: if define then used as request body. None by default. 371 self.timeout: global request timeout, 15 seconds by default. 372 :param url: url with REST request. 373 :param reqType: send "GET" or "POST" request. "GET" by default. 374 :param retry: how many times retry after first request if an 5xx server errors occurred. 375 :param pause: sleep time in seconds between retries. 376 :return: response JSON (dictionary) from broker. 377 """ 378 if reqType not in ("GET", "POST"): 379 uLogger.error("You can define request type: 'GET' or 'POST'!") 380 raise Exception("Incorrect value") 381 382 if self.moreDebug: 383 uLogger.debug("Request parameters:") 384 uLogger.debug(" - REST API URL: {}".format(url)) 385 uLogger.debug(" - request type: {}".format(reqType)) 386 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 387 uLogger.debug(" - body:\n{}".format(self.body)) 388 389 # fast hack to avoid all operations with some tickers/FIGI 390 responseJSON = {} 391 oK = True 392 for item in self.exclude: 393 if item in url: 394 if self.moreDebug: 395 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 396 397 oK = False 398 break 399 400 if oK: 401 counter = 0 402 response = None 403 errMsg = "" 404 405 while not response and counter <= retry: 406 if reqType == "GET": 407 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 408 409 if reqType == "POST": 410 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 411 412 if self.moreDebug: 413 uLogger.debug("Response:") 414 uLogger.debug(" - status code: {}".format(response.status_code)) 415 uLogger.debug(" - reason: {}".format(response.reason)) 416 uLogger.debug(" - body length: {}".format(len(response.text))) 417 uLogger.debug(" - headers:\n{}".format(response.headers)) 418 419 # Server returns some headers: 420 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 421 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 422 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 423 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 424 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 425 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 426 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 427 sleep(rateLimitWait) 428 429 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 430 if 400 <= response.status_code < 500: 431 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 432 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 433 counter = retry + 1 434 435 if 500 <= response.status_code < 600: 436 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 437 uLogger.debug(" - not oK, {}".format(errMsg)) 438 counter += 1 439 440 if counter <= retry: 441 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 442 sleep(pause) 443 444 responseJSON = self._ParseJSON(rawData=response.text) 445 446 if errMsg: 447 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 448 uLogger.error(" - not oK, {}".format(errMsg)) 449 450 return responseJSON 451 452 def _IUpdater(self, iType: str) -> tuple: 453 """ 454 Request instrument by type from server. See available API methods for instruments: 455 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 456 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 457 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 458 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 459 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 460 461 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 462 :return: tuple with iType name and list of available instruments of current type for defined user token. 463 """ 464 result = [] 465 466 if iType in TKS_INSTRUMENTS: 467 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 468 469 # all instruments have the same body in API v2 requests: 470 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 471 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 472 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 473 474 return iType, result 475 476 def _IWrapper(self, kwargs): 477 """ 478 Wrapper runs instrument's update method `_IUpdater()`. 479 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 480 """ 481 return self._IUpdater(**kwargs) 482 483 def Listing(self) -> dict: 484 """ 485 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 486 487 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 488 """ 489 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 490 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 491 492 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 493 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 494 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 495 496 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 497 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 498 poolUpdater.close() 499 500 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 501 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 502 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 503 504 # calculate minimum price increment (step) for all instruments and set up instrument's type: 505 for iType in iList.keys(): 506 for ticker in iList[iType]: 507 iList[iType][ticker]["type"] = iType 508 509 if "minPriceIncrement" in iList[iType][ticker].keys(): 510 iList[iType][ticker]["step"] = NanoToFloat( 511 iList[iType][ticker]["minPriceIncrement"]["units"], 512 iList[iType][ticker]["minPriceIncrement"]["nano"], 513 ) 514 515 else: 516 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 517 518 return iList 519 520 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 521 """ 522 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 523 524 See also: `DumpInstruments()`, `Listing()`. 525 526 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 527 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 528 """ 529 if self.iListDumpFile is None or not self.iListDumpFile: 530 uLogger.error("Output name of dump file must be defined!") 531 raise Exception("Filename required") 532 533 if not self.iList or forceUpdate: 534 self.iList = self.Listing() 535 536 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 537 538 # Save as XLSX with separated sheets for every type of instruments: 539 with pd.ExcelWriter( 540 path=xlsxDumpFile, 541 date_format=TKS_DATE_FORMAT, 542 datetime_format=TKS_DATE_TIME_FORMAT, 543 mode="w", 544 ) as writer: 545 for iType in TKS_INSTRUMENTS: 546 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 547 df = df[sorted(df)] # sorted by column names 548 df = df.applymap( 549 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 550 na_action="ignore", 551 ) # converting numbers from nano-type to float in every cell 552 df.to_excel( 553 writer, 554 sheet_name=iType, 555 encoding="UTF-8", 556 freeze_panes=(1, 1), 557 ) # saving as XLSX-file with freeze first row and column as headers 558 559 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 560 561 def DumpInstruments(self, forceUpdate: bool = True) -> str: 562 """ 563 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 564 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 565 566 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 567 568 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 569 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 570 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 571 """ 572 if self.iListDumpFile is None or not self.iListDumpFile: 573 uLogger.error("Output name of dump file must be defined!") 574 raise Exception("Filename required") 575 576 if not self.iList or forceUpdate: 577 self.iList = self.Listing() 578 579 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 580 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 581 fH.write(jsonDump) 582 583 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 584 585 return jsonDump 586 587 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 588 """ 589 Show information about one instrument defined by json data and prints it in Markdown format. 590 591 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 592 593 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 594 :param show: if `True` then also printing information about instrument and its current price. 595 :return: multilines text in Markdown format with information about one instrument. 596 """ 597 splitLine = "| | |\n" 598 infoText = "" 599 600 if iJSON is not None and iJSON and isinstance(iJSON, dict): 601 info = [ 602 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 603 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 604 "| Parameters | Values |\n", 605 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 606 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 607 "| Full name: | {:<54} |\n".format(iJSON["name"]), 608 ] 609 610 if "sector" in iJSON.keys() and iJSON["sector"]: 611 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 612 613 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 614 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 615 616 info.extend([ 617 splitLine, 618 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 619 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 620 ]) 621 622 if "isin" in iJSON.keys() and iJSON["isin"]: 623 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 624 625 if "classCode" in iJSON.keys(): 626 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 627 628 info.extend([ 629 splitLine, 630 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 631 splitLine, 632 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 633 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 634 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 635 ]) 636 637 if iJSON["figi"]: 638 self.figi = iJSON["figi"] 639 iJSON = iJSON | self.RequestTradingStatus() 640 641 info.extend([ 642 splitLine, 643 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 644 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 645 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 646 ]) 647 648 info.append(splitLine) 649 650 if "type" in iJSON.keys() and iJSON["type"]: 651 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 652 653 if "shareType" in iJSON.keys() and iJSON["shareType"]: 654 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 655 656 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 657 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 658 659 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 660 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 661 662 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 663 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 664 665 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 666 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 667 668 if "focusType" in iJSON.keys() and iJSON["focusType"]: 669 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 670 671 if "assetType" in iJSON.keys() and iJSON["assetType"]: 672 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 673 674 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 675 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 676 677 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 678 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 679 680 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 681 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 682 683 if "currency" in iJSON.keys(): 684 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 685 686 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 687 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 688 689 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 690 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 691 692 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 693 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 694 695 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 696 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 697 698 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 699 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 700 701 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 702 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 703 704 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 705 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 706 707 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 708 info.append("| Perpetual bond: | Yes |\n") 709 710 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 711 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 712 713 iExt = None 714 if iJSON["type"] == "Bonds": 715 info.extend([ 716 splitLine, 717 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 718 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 719 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 720 iJSON["nominal"]["currency"], 721 )), 722 ]) 723 724 if "floatingCouponFlag" in iJSON.keys(): 725 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 726 727 if "amortizationFlag" in iJSON.keys(): 728 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 729 730 info.append(splitLine) 731 732 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 733 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 734 735 if iJSON["figi"]: 736 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 737 738 info.extend([ 739 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 740 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 741 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 742 ]) 743 744 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 745 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 746 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 747 iJSON["aciValue"]["currency"] 748 ))) 749 750 if "currentPrice" in iJSON.keys(): 751 info.append(splitLine) 752 753 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 754 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 755 756 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 757 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 758 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 759 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 760 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 761 762 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 763 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 764 765 info.extend([ 766 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 767 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 768 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 769 )), 770 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 771 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 772 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 773 )), 774 "| Changes between last deal price and last close | {:<54} |\n".format( 775 "{:.2f}%{}".format( 776 iJSON["currentPrice"]["changes"], 777 " ({}{:.2f} {})".format( 778 "+" if bondChangesDelta > 0 else "", 779 bondChangesDelta, 780 aciCurrency 781 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 782 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 783 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 784 currency 785 ), 786 ) 787 ), 788 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 789 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 790 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 791 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 792 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 793 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 794 )), 795 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 796 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 797 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 798 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 799 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 800 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 801 )), 802 ]) 803 804 if "lot" in iJSON.keys(): 805 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 806 807 if "step" in iJSON.keys() and iJSON["step"] != 0: 808 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 809 810 # Add bond payment calendar: 811 if iJSON["type"] == "Bonds": 812 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 813 info.extend(["\n", strCalendar]) 814 815 infoText += "".join(info) 816 817 if show: 818 uLogger.info("{}".format(infoText)) 819 820 else: 821 uLogger.debug("{}".format(infoText)) 822 823 if self.infoFile is not None: 824 with open(self.infoFile, "w", encoding="UTF-8") as fH: 825 fH.write(infoText) 826 827 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 828 829 return infoText 830 831 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 832 """ 833 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 834 835 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 836 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 837 :return: JSON formatted data with information about instrument. 838 """ 839 tickerJSON = {} 840 if self.moreDebug: 841 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 842 843 if not self.ticker: 844 uLogger.warning("self.ticker variable is not be empty!") 845 846 else: 847 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 848 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 849 raise Exception("Instrument not allowed") 850 851 if not self.iList: 852 self.iList = self.Listing() 853 854 if self.ticker in self.iList["Shares"].keys(): 855 tickerJSON = self.iList["Shares"][self.ticker] 856 if self.moreDebug: 857 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 858 859 elif self.ticker in self.iList["Currencies"].keys(): 860 tickerJSON = self.iList["Currencies"][self.ticker] 861 if self.moreDebug: 862 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 863 864 elif self.ticker in self.iList["Bonds"].keys(): 865 tickerJSON = self.iList["Bonds"][self.ticker] 866 if self.moreDebug: 867 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 868 869 elif self.ticker in self.iList["Etfs"].keys(): 870 tickerJSON = self.iList["Etfs"][self.ticker] 871 if self.moreDebug: 872 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 873 874 elif self.ticker in self.iList["Futures"].keys(): 875 tickerJSON = self.iList["Futures"][self.ticker] 876 if self.moreDebug: 877 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 878 879 if tickerJSON: 880 self.figi = tickerJSON["figi"] 881 882 if requestPrice: 883 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 884 885 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 886 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 887 888 else: 889 tickerJSON["currentPrice"]["changes"] = 0 890 891 if show: 892 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 893 894 else: 895 if show: 896 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 897 898 return tickerJSON 899 900 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 901 """ 902 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 903 904 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 905 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 906 :return: JSON formatted data with information about instrument. 907 """ 908 figiJSON = {} 909 if self.moreDebug: 910 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 911 912 if not self.figi: 913 uLogger.warning("self.figi variable is not be empty!") 914 915 else: 916 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 917 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 918 raise Exception("Instrument not allowed") 919 920 if not self.iList: 921 self.iList = self.Listing() 922 923 for item in self.iList["Shares"].keys(): 924 if self.figi == self.iList["Shares"][item]["figi"]: 925 figiJSON = self.iList["Shares"][item] 926 927 if self.moreDebug: 928 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 929 930 break 931 932 if not figiJSON: 933 for item in self.iList["Currencies"].keys(): 934 if self.figi == self.iList["Currencies"][item]["figi"]: 935 figiJSON = self.iList["Currencies"][item] 936 937 if self.moreDebug: 938 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 939 940 break 941 942 if not figiJSON: 943 for item in self.iList["Bonds"].keys(): 944 if self.figi == self.iList["Bonds"][item]["figi"]: 945 figiJSON = self.iList["Bonds"][item] 946 947 if self.moreDebug: 948 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 949 950 break 951 952 if not figiJSON: 953 for item in self.iList["Etfs"].keys(): 954 if self.figi == self.iList["Etfs"][item]["figi"]: 955 figiJSON = self.iList["Etfs"][item] 956 957 if self.moreDebug: 958 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 959 960 break 961 962 if not figiJSON: 963 for item in self.iList["Futures"].keys(): 964 if self.figi == self.iList["Futures"][item]["figi"]: 965 figiJSON = self.iList["Futures"][item] 966 967 if self.moreDebug: 968 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 969 970 break 971 972 if figiJSON: 973 self.figi = figiJSON["figi"] 974 self.ticker = figiJSON["ticker"] 975 976 if requestPrice: 977 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 978 979 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 980 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 981 982 else: 983 figiJSON["currentPrice"]["changes"] = 0 984 985 if show: 986 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 987 988 else: 989 if show: 990 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 991 992 return figiJSON 993 994 def GetCurrentPrices(self, show: bool = True) -> dict: 995 """ 996 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 997 `{"buy": [{"price": 1243.8, "quantity": 193}, 998 {"price": 1244.0, "quantity": 168}, 999 {"price": 1244.8, "quantity": 5}, 1000 {"price": 1245.0, "quantity": 61}, 1001 {"price": 1245.4, "quantity": 60}], 1002 "sell": [{"price": 1243.6, "quantity": 8}, 1003 {"price": 1242.6, "quantity": 10}, 1004 {"price": 1242.4, "quantity": 18}, 1005 {"price": 1242.2, "quantity": 50}, 1006 {"price": 1242.0, "quantity": 113}], 1007 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1008 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1009 - sell: list of dicts with Buyers prices, 1010 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1011 - quantity: volume value by current price in lots, 1012 - limitUp: current trade session limit price, maximum, 1013 - limitDown: current trade session limit price, minimum, 1014 - lastPrice: last deal price of the instrument, 1015 - closePrice: previous trade session close price of the instrument. 1016 1017 See also: `SearchByTicker()` and `SearchByFIGI()`. 1018 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1019 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1020 1021 :param show: if `True` then print DOM to log and console. 1022 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1023 If an error occurred then returns an empty record: 1024 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1025 """ 1026 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1027 1028 if self.depth < 1: 1029 uLogger.error("Depth of Market (DOM) must be >=1!") 1030 raise Exception("Incorrect value") 1031 1032 if not (self.ticker or self.figi): 1033 uLogger.error("self.ticker or self.figi variables must be defined!") 1034 raise Exception("Ticker or FIGI required") 1035 1036 if self.ticker and not self.figi: 1037 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1038 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1039 1040 if not self.ticker and self.figi: 1041 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1042 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1043 1044 if not self.figi: 1045 uLogger.error("FIGI is not defined!") 1046 raise Exception("Ticker or FIGI required") 1047 1048 else: 1049 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1050 1051 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1052 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1053 self.body = str({"figi": self.figi, "depth": self.depth}) 1054 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1055 1056 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1057 # list of dicts with sellers orders: 1058 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1059 1060 # list of dicts with buyers orders: 1061 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1062 1063 # max price of instrument at this time: 1064 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1065 1066 # min price of instrument at this time: 1067 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1068 1069 # last price of deal with instrument: 1070 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1071 1072 # last close price of instrument: 1073 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1074 1075 else: 1076 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1077 uLogger.debug("Server response: {}".format(pricesResponse)) 1078 1079 if show: 1080 if prices["buy"] or prices["sell"]: 1081 info = [ 1082 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1083 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1084 self.ticker, 1085 self.figi, 1086 self.depth, 1087 ), 1088 "-" * 60, "\n", 1089 " Orders of Buyers | Orders of Sellers\n", 1090 "-" * 60, "\n", 1091 " Sell prices (volumes) | Buy prices (volumes)\n", 1092 "-" * 60, "\n", 1093 ] 1094 1095 if not prices["buy"]: 1096 info.append(" | No orders!\n") 1097 sumBuy = 0 1098 1099 else: 1100 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1101 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1102 for item in maxMinSorted: 1103 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1104 1105 if not prices["sell"]: 1106 info.append("No orders! |\n") 1107 sumSell = 0 1108 1109 else: 1110 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1111 for item in prices["sell"]: 1112 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1113 1114 info.extend([ 1115 "-" * 60, "\n", 1116 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1117 "-" * 60, "\n", 1118 ]) 1119 1120 infoText = "".join(info) 1121 1122 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1123 1124 else: 1125 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1126 1127 return prices 1128 1129 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1130 """ 1131 This method get and show information about all available broker instruments for current user account. 1132 If `instrumentsFile` string is not empty then also save information to this file. 1133 1134 :param show: if `True` then print results to console, if `False` — print only to file. 1135 :return: multi-lines string with all available broker instruments 1136 """ 1137 if not self.iList: 1138 self.iList = self.Listing() 1139 1140 info = [ 1141 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1142 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1143 ] 1144 1145 # add instruments count by type: 1146 for iType in self.iList.keys(): 1147 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1148 1149 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1150 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1151 1152 # generating info tables with all instruments by type: 1153 for iType in self.iList.keys(): 1154 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1155 1156 for instrument in self.iList[iType].keys(): 1157 iName = self.iList[iType][instrument]["name"] # instrument's name 1158 if len(iName) > 57: 1159 iName = "{}...".format(iName[:54]) # right trim for a long string 1160 1161 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1162 self.iList[iType][instrument]["ticker"], 1163 iName, 1164 self.iList[iType][instrument]["figi"], 1165 self.iList[iType][instrument]["currency"], 1166 self.iList[iType][instrument]["lot"], 1167 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1168 )) 1169 1170 infoText = "".join(info) 1171 1172 if show: 1173 uLogger.info(infoText) 1174 1175 if self.instrumentsFile: 1176 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1177 fH.write(infoText) 1178 1179 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1180 1181 return infoText 1182 1183 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1184 """ 1185 This method search and show information about instruments by part of its ticker, FIGI or name. 1186 If `searchResultsFile` string is not empty then also save information to this file. 1187 1188 :param pattern: string with part of ticker, FIGI or instrument's name. 1189 :param show: if `True` then print results to console, if `False` — return list of result only. 1190 :return: list of dictionaries with all found instruments. 1191 """ 1192 if not self.iList: 1193 self.iList = self.Listing() 1194 1195 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1196 compiledPattern = re.compile(pattern, re.IGNORECASE) 1197 1198 for iType in self.iList: 1199 for instrument in self.iList[iType].values(): 1200 searchResult = compiledPattern.search(" ".join( 1201 [instrument["ticker"], instrument["figi"], instrument["name"]] 1202 )) 1203 1204 if searchResult: 1205 searchResults[iType][instrument["ticker"]] = instrument 1206 1207 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1208 info = [ 1209 "# Search results\n\n", 1210 "* **Search pattern:** [{}]\n".format(pattern), 1211 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1212 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1213 ] 1214 infoShort = info[:] 1215 1216 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1217 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1218 skippedLine = "| ... | ... | ... | ... |\n" 1219 1220 if resultsLen == 0: 1221 info.append("\nNo results\n") 1222 infoShort.append("\nNo results\n") 1223 uLogger.warning("No results. Try changing your search pattern.") 1224 1225 else: 1226 for iType in searchResults: 1227 iTypeValuesCount = len(searchResults[iType].values()) 1228 if iTypeValuesCount > 0: 1229 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1230 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1231 1232 for instrument in searchResults[iType].values(): 1233 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1234 instrument["type"], 1235 instrument["ticker"], 1236 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1237 instrument["figi"], 1238 )) 1239 1240 if iTypeValuesCount <= 5: 1241 infoShort.extend(info[-iTypeValuesCount:]) 1242 1243 else: 1244 infoShort.extend(info[-5:]) 1245 infoShort.append(skippedLine) 1246 1247 infoText = "".join(info) 1248 infoTextShort = "".join(infoShort) 1249 1250 if show: 1251 uLogger.info(infoTextShort) 1252 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1253 1254 if self.searchResultsFile: 1255 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1256 fH.write(infoText) 1257 1258 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1259 1260 return searchResults 1261 1262 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1263 """ 1264 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1265 1266 :param instruments: list of strings with tickers or FIGIs. 1267 :return: list with unique instrument FIGIs only. 1268 """ 1269 requestedInstruments = [] 1270 for iName in instruments: 1271 if iName not in self.aliases.keys(): 1272 if iName not in requestedInstruments: 1273 requestedInstruments.append(iName) 1274 1275 else: 1276 if iName not in requestedInstruments: 1277 if self.aliases[iName] not in requestedInstruments: 1278 requestedInstruments.append(self.aliases[iName]) 1279 1280 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1281 1282 onlyUniqueFIGIs = [] 1283 for iName in requestedInstruments: 1284 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1285 continue 1286 1287 self.ticker = iName 1288 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1289 1290 if not iData: 1291 self.ticker = "" 1292 self.figi = iName 1293 1294 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1295 1296 if not iData: 1297 self.figi = "" 1298 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1299 1300 if iData and iData["figi"] not in onlyUniqueFIGIs: 1301 onlyUniqueFIGIs.append(iData["figi"]) 1302 1303 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1304 1305 return onlyUniqueFIGIs 1306 1307 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1308 """ 1309 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1310 1311 See limits: https://tinkoff.github.io/investAPI/limits/ 1312 1313 If `pricesFile` string is not empty then also save information to this file. 1314 1315 :param instruments: list of strings with tickers or FIGIs. 1316 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1317 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1318 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1319 """ 1320 if instruments is None or not instruments: 1321 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1322 raise Exception("Ticker or FIGI required") 1323 1324 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1325 1326 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1327 1328 iList = [] # trying to get info and current prices about all unique instruments: 1329 for self.figi in onlyUniqueFIGIs: 1330 iData = self.SearchByFIGI(requestPrice=True) 1331 iList.append(iData) 1332 1333 self.ShowListOfPrices(iList, show) 1334 1335 return iList 1336 1337 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1338 """ 1339 Show table contains current prices of given instruments. 1340 1341 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1342 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1343 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1344 :return: multilines text in Markdown format as a table contains current prices. 1345 """ 1346 infoText = "" 1347 1348 if show or self.pricesFile: 1349 info = [ 1350 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1351 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1352 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1353 ] 1354 1355 for item in iList: 1356 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1357 item["ticker"], 1358 item["figi"], 1359 item["type"], 1360 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1361 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1362 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1363 "{} / {}".format( 1364 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1365 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1366 ), 1367 "{} / {}".format( 1368 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1369 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1370 ), 1371 item["currency"], 1372 )) 1373 1374 infoText = "".join(info) 1375 1376 if show: 1377 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1378 1379 if self.pricesFile: 1380 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1381 fH.write(infoText) 1382 1383 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1384 1385 return infoText 1386 1387 def RequestTradingStatus(self) -> dict: 1388 """ 1389 Requesting trading status for the instrument defined by `figi` variable. 1390 1391 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1392 1393 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1394 1395 :return: dictionary with trading status attributes. Response example: 1396 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1397 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1398 """ 1399 if self.figi is None or not self.figi: 1400 uLogger.error("Variable `figi` must be defined for using this method!") 1401 raise Exception("FIGI required") 1402 1403 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1404 1405 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1406 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1407 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1408 1409 if self.moreDebug: 1410 uLogger.debug("Records about current trading status successfully received") 1411 1412 return tradingStatus 1413 1414 def RequestPortfolio(self) -> dict: 1415 """ 1416 Requesting actual user's portfolio for current `accountId`. 1417 1418 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1419 1420 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1421 1422 :return: dictionary with user's portfolio. 1423 """ 1424 if self.accountId is None or not self.accountId: 1425 uLogger.error("Variable `accountId` must be defined for using this method!") 1426 raise Exception("Account ID required") 1427 1428 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1429 1430 self.body = str({"accountId": self.accountId}) 1431 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1432 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1433 1434 if self.moreDebug: 1435 uLogger.debug("Records about user's portfolio successfully received") 1436 1437 return rawPortfolio 1438 1439 def RequestPositions(self) -> dict: 1440 """ 1441 Requesting open positions by currencies and instruments for current `accountId`. 1442 1443 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1444 1445 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1446 1447 :return: dictionary with open positions by instruments. 1448 """ 1449 if self.accountId is None or not self.accountId: 1450 uLogger.error("Variable `accountId` must be defined for using this method!") 1451 raise Exception("Account ID required") 1452 1453 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1454 1455 self.body = str({"accountId": self.accountId}) 1456 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1457 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1458 1459 if self.moreDebug: 1460 uLogger.debug("Records about current open positions successfully received") 1461 1462 return rawPositions 1463 1464 def RequestPendingOrders(self) -> list: 1465 """ 1466 Requesting current actual pending orders for current `accountId`. 1467 1468 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1469 1470 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1471 1472 :return: list of dictionaries with pending orders. 1473 """ 1474 if self.accountId is None or not self.accountId: 1475 uLogger.error("Variable `accountId` must be defined for using this method!") 1476 raise Exception("Account ID required") 1477 1478 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1479 1480 self.body = str({"accountId": self.accountId}) 1481 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1482 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1483 1484 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1485 1486 return rawOrders 1487 1488 def RequestStopOrders(self) -> list: 1489 """ 1490 Requesting current actual stop orders for current `accountId`. 1491 1492 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1493 1494 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1495 1496 :return: list of dictionaries with stop orders. 1497 """ 1498 if self.accountId is None or not self.accountId: 1499 uLogger.error("Variable `accountId` must be defined for using this method!") 1500 raise Exception("Account ID required") 1501 1502 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1503 1504 self.body = str({"accountId": self.accountId}) 1505 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1506 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1507 1508 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1509 1510 return rawStopOrders 1511 1512 def Overview(self, show: bool = False, details: str = "full") -> dict: 1513 """ 1514 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1515 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1516 and `overviewBondsCalendarFile` are defined then also save information to file. 1517 1518 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1519 many requests about the state of the portfolio, and then, based on the received data, a large number 1520 of calculation and statistics are collected. 1521 1522 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1523 :param details: how detailed should the information be? 1524 - `full` — shows full available information about portfolio status (by default), 1525 - `positions` — shows only open positions, 1526 - `orders` — shows only sections of open limits and stop orders. 1527 - `digest` — show a short digest of the portfolio status, 1528 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1529 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1530 :return: dictionary with client's raw portfolio and some statistics. 1531 """ 1532 if self.accountId is None or not self.accountId: 1533 uLogger.error("Variable `accountId` must be defined for using this method!") 1534 raise Exception("Account ID required") 1535 1536 view = { 1537 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1538 "headers": {}, # list of dictionaries, response headers without "positions" section 1539 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1540 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1541 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1542 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1543 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1544 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1545 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1546 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1547 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1548 }, 1549 "stat": { # --- some statistics calculated using "raw" sections: 1550 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1551 "availableRUB": 0., # available rubles (without other currencies) 1552 "blockedRUB": 0., # blocked sum in Russian Rouble 1553 "totalChangesRUB": 0., # changes for all open trades in RUB 1554 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1555 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1556 "sharesCostRUB": 0., # costs of all shares in RUB 1557 "bondsCostRUB": 0., # costs of all bonds in RUB 1558 "etfsCostRUB": 0., # costs of all etfs in RUB 1559 "futuresCostRUB": 0., # costs of all futures in RUB 1560 "Currencies": [], # list of dictionaries of all currencies statistics 1561 "Shares": [], # list of dictionaries of all shares statistics 1562 "Bonds": [], # list of dictionaries of all bonds statistics 1563 "Etfs": [], # list of dictionaries of all etfs statistics 1564 "Futures": [], # list of dictionaries of all futures statistics 1565 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1566 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1567 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1568 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1569 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1570 }, 1571 "analytics": { # --- some analytics of portfolio: 1572 "distrByAssets": {}, # portfolio distribution by assets 1573 "distrByCompanies": {}, # portfolio distribution by companies 1574 "distrBySectors": {}, # portfolio distribution by sectors 1575 "distrByCurrencies": {}, # portfolio distribution by currencies 1576 "distrByCountries": {}, # portfolio distribution by countries 1577 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1578 } 1579 } 1580 1581 details = details.lower() 1582 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1583 if details not in availableDetails: 1584 details = "full" 1585 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1586 1587 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1588 1589 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1590 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1591 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1592 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1593 1594 # save response headers without "positions" section: 1595 for key in portfolioResponse.keys(): 1596 if key != "positions": 1597 view["raw"]["headers"][key] = portfolioResponse[key] 1598 1599 else: 1600 continue 1601 1602 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1603 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1604 for item in portfolioResponse["positions"]: 1605 if item["instrumentType"] == "currency": 1606 self.figi = item["figi"] 1607 curr = self.SearchByFIGI(requestPrice=False) 1608 1609 # current price of currency in RUB: 1610 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1611 "name": curr["name"], 1612 "currentPrice": NanoToFloat( 1613 item["currentPrice"]["units"], 1614 item["currentPrice"]["nano"] 1615 ), 1616 } 1617 1618 view["raw"]["Currencies"].append(item) 1619 1620 elif item["instrumentType"] == "share": 1621 view["raw"]["Shares"].append(item) 1622 1623 elif item["instrumentType"] == "bond": 1624 view["raw"]["Bonds"].append(item) 1625 1626 elif item["instrumentType"] == "etf": 1627 view["raw"]["Etfs"].append(item) 1628 1629 elif item["instrumentType"] == "futures": 1630 view["raw"]["Futures"].append(item) 1631 1632 else: 1633 continue 1634 1635 # how many volume of currencies (by ISO currency name) are blocked: 1636 for item in view["raw"]["positions"]["blocked"]: 1637 blocked = NanoToFloat(item["units"], item["nano"]) 1638 if blocked > 0: 1639 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1640 1641 # how many volume of instruments (by FIGI) are blocked: 1642 for item in view["raw"]["positions"]["securities"]: 1643 blocked = int(item["blocked"]) 1644 if blocked > 0: 1645 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1646 1647 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1648 1649 if "rub" in allBlocked.keys(): 1650 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1651 1652 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1653 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1654 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1655 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1656 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1657 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1658 view["stat"]["portfolioCostRUB"] = sum([ 1659 view["stat"]["allCurrenciesCostRUB"], 1660 view["stat"]["sharesCostRUB"], 1661 view["stat"]["bondsCostRUB"], 1662 view["stat"]["etfsCostRUB"], 1663 view["stat"]["futuresCostRUB"], 1664 ]) 1665 1666 # --- calculating some portfolio statistics: 1667 byComp = {} # distribution by companies 1668 bySect = {} # distribution by sectors 1669 byCurr = {} # distribution by currencies (include RUB) 1670 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1671 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1672 1673 for item in portfolioResponse["positions"]: 1674 self.figi = item["figi"] 1675 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1676 1677 if instrument: 1678 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1679 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1680 1681 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1682 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1683 1684 else: 1685 blocked = 0 1686 1687 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1688 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1689 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1690 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1691 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1692 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1693 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1694 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1695 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1696 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1697 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1698 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1699 1700 statData = { 1701 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1702 "ticker": instrument["ticker"], # ticker by FIGI 1703 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1704 "volume": volume, # available volume of instrument 1705 "lots": lots, # volume in lots of instrument 1706 "direction": direction, # direction of an instrument's position: short or long 1707 "blocked": blocked, # blocked volume of currency or instrument 1708 "currentPrice": curPrice, # current instrument's price in basic asset 1709 "average": average, # current average position price 1710 "cost": cost, # current cost of all volume of instrument in basic asset 1711 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1712 "costRUB": costRUB, # cost of instrument in ruble 1713 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1714 "profit": profit, # expected profit at current moment 1715 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1716 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1717 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1718 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1719 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1720 "step": instrument["step"], # minimum price increment 1721 } 1722 1723 # adding distribution by unique countries: 1724 if statData["country"] not in byCountry.keys(): 1725 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1726 1727 else: 1728 byCountry[statData["country"]]["cost"] += costRUB 1729 byCountry[statData["country"]]["percent"] += percentCostRUB 1730 1731 if item["instrumentType"] != "currency": 1732 # adding distribution by unique companies: 1733 if statData["name"]: 1734 if statData["name"] not in byComp.keys(): 1735 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1736 1737 else: 1738 byComp[statData["name"]]["cost"] += costRUB 1739 byComp[statData["name"]]["percent"] += percentCostRUB 1740 1741 # adding distribution by unique sectors: 1742 if statData["sector"] not in bySect.keys(): 1743 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1744 1745 else: 1746 bySect[statData["sector"]]["cost"] += costRUB 1747 bySect[statData["sector"]]["percent"] += percentCostRUB 1748 1749 # adding distribution by unique currencies: 1750 if currency not in byCurr.keys(): 1751 byCurr[currency] = { 1752 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1753 "cost": costRUB, 1754 "percent": percentCostRUB 1755 } 1756 1757 else: 1758 byCurr[currency]["cost"] += costRUB 1759 byCurr[currency]["percent"] += percentCostRUB 1760 1761 # saving statistics for every instrument: 1762 if item["instrumentType"] == "currency": 1763 view["stat"]["Currencies"].append(statData) 1764 1765 # update dict with free funds for trading (total - blocked) by currencies 1766 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1767 view["stat"]["funds"][currency] = { 1768 "total": volume, 1769 "totalCostRUB": costRUB, # total volume cost in rubles 1770 "free": volume - blocked, 1771 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1772 } 1773 1774 elif item["instrumentType"] == "share": 1775 view["stat"]["Shares"].append(statData) 1776 1777 elif item["instrumentType"] == "bond": 1778 view["stat"]["Bonds"].append(statData) 1779 1780 elif item["instrumentType"] == "etf": 1781 view["stat"]["Etfs"].append(statData) 1782 1783 elif item["instrumentType"] == "Futures": 1784 view["stat"]["Futures"].append(statData) 1785 1786 else: 1787 continue 1788 1789 # total changes in Russian Ruble: 1790 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1791 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1792 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1793 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1794 view["stat"]["funds"]["rub"] = { 1795 "total": view["stat"]["availableRUB"], 1796 "totalCostRUB": view["stat"]["availableRUB"], 1797 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1798 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1799 } 1800 1801 # --- pending orders sector data: 1802 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending orders to avoid many times price requests 1803 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1804 1805 for item in view["raw"]["orders"]: 1806 self.figi = item["figi"] 1807 1808 if item["figi"] not in uniquePendingOrdersFIGIs: 1809 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1810 1811 uniquePendingOrdersFIGIs.append(item["figi"]) 1812 uniquePendingOrders[item["figi"]] = instrument 1813 1814 else: 1815 instrument = uniquePendingOrders[item["figi"]] 1816 1817 if instrument: 1818 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1819 orderType = TKS_ORDER_TYPES[item["orderType"]] 1820 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1821 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1822 1823 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1824 if item["direction"] == "ORDER_DIRECTION_BUY": 1825 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1826 1827 else: 1828 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1829 1830 # requested price for order execution: 1831 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1832 1833 # necessary changes in percent to reach target from current price: 1834 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1835 1836 view["stat"]["orders"].append({ 1837 "orderID": item["orderId"], # orderId number parameter of current order 1838 "figi": item["figi"], # FIGI identification 1839 "ticker": instrument["ticker"], # ticker name by FIGI 1840 "lotsRequested": item["lotsRequested"], # requested lots value 1841 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1842 "currentPrice": lastPrice, # current instrument's price for defined action 1843 "targetPrice": target, # requested price for order execution in base currency 1844 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1845 "percentChanges": changes, # changes in percent to target from current price 1846 "currency": item["currency"], # instrument's currency name 1847 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1848 "type": orderType, # type of order from TKS_ORDER_TYPES 1849 "status": orderState, # order status from TKS_ORDER_STATES 1850 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1851 }) 1852 1853 # --- stop orders sector data: 1854 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1855 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1856 1857 for item in view["raw"]["stopOrders"]: 1858 self.figi = item["figi"] 1859 1860 if item["figi"] not in uniqueStopOrdersFIGIs: 1861 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1862 1863 uniqueStopOrdersFIGIs.append(item["figi"]) 1864 uniqueStopOrders[item["figi"]] = instrument 1865 1866 else: 1867 instrument = uniqueStopOrders[item["figi"]] 1868 1869 if instrument: 1870 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1871 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1872 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1873 1874 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1875 if "expirationTime" in item.keys(): 1876 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1877 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1878 1879 else: 1880 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1881 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1882 1883 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1884 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1885 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1886 1887 else: 1888 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1889 1890 # requested price when stop-order executed: 1891 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1892 1893 # price for limit-order, set up when stop-order executed: 1894 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1895 1896 # necessary changes in percent to reach target from current price: 1897 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1898 1899 view["stat"]["stopOrders"].append({ 1900 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1901 "figi": item["figi"], # FIGI identification 1902 "ticker": instrument["ticker"], # ticker name by FIGI 1903 "lotsRequested": item["lotsRequested"], # requested lots value 1904 "currentPrice": lastPrice, # current instrument's price for defined action 1905 "targetPrice": target, # requested price for stop-order execution in base currency 1906 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1907 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1908 "percentChanges": changes, # changes in percent to target from current price 1909 "currency": item["currency"], # instrument's currency name 1910 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1911 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1912 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1913 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1914 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1915 }) 1916 1917 # --- calculating data for analytics section: 1918 # portfolio distribution by assets: 1919 view["analytics"]["distrByAssets"] = { 1920 "Ruble": { 1921 "uniques": 1, 1922 "cost": view["stat"]["availableRUB"], 1923 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1924 }, 1925 "Currencies": { 1926 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 1927 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 1928 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1929 }, 1930 "Shares": { 1931 "uniques": len(view["stat"]["Shares"]), 1932 "cost": view["stat"]["sharesCostRUB"], 1933 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1934 }, 1935 "Bonds": { 1936 "uniques": len(view["stat"]["Bonds"]), 1937 "cost": view["stat"]["bondsCostRUB"], 1938 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1939 }, 1940 "Etfs": { 1941 "uniques": len(view["stat"]["Etfs"]), 1942 "cost": view["stat"]["etfsCostRUB"], 1943 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1944 }, 1945 "Futures": { 1946 "uniques": len(view["stat"]["Futures"]), 1947 "cost": view["stat"]["futuresCostRUB"], 1948 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1949 }, 1950 } 1951 1952 # portfolio distribution by companies: 1953 view["analytics"]["distrByCompanies"]["All money cash"] = { 1954 "ticker": "", 1955 "cost": view["stat"]["allCurrenciesCostRUB"], 1956 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1957 } 1958 view["analytics"]["distrByCompanies"].update(byComp) 1959 1960 # portfolio distribution by sectors: 1961 view["analytics"]["distrBySectors"]["All money cash"] = { 1962 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 1963 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 1964 } 1965 view["analytics"]["distrBySectors"].update(bySect) 1966 1967 # portfolio distribution by currencies: 1968 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 1969 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 1970 1971 if self.moreDebug: 1972 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 1973 1974 view["analytics"]["distrByCurrencies"].update(byCurr) 1975 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 1976 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 1977 1978 # portfolio distribution by countries: 1979 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 1980 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 1981 1982 if self.moreDebug: 1983 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 1984 1985 view["analytics"]["distrByCountries"].update(byCountry) 1986 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 1987 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 1988 1989 # --- Prepare text statistics overview in human-readable: 1990 if show: 1991 # Whatever the value `details`, header not changes: 1992 info = [ 1993 "# Client's portfolio\n\n", 1994 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1995 "* **Account ID:** [{}]\n".format(self.accountId), 1996 ] 1997 1998 if details in ["full", "positions", "digest"]: 1999 info.extend([ 2000 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2001 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2002 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2003 view["stat"]["totalChangesRUB"], 2004 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2005 view["stat"]["totalChangesPercentRUB"], 2006 ), 2007 ]) 2008 2009 if details in ["full", "positions"]: 2010 info.extend([ 2011 "## Open positions\n\n", 2012 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2013 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2014 "| Ruble | {:>31} | | | | | |\n".format( 2015 "{:.2f} ({:.2f}) rub".format( 2016 view["stat"]["availableRUB"], 2017 view["stat"]["blockedRUB"], 2018 ) 2019 ) 2020 ]) 2021 2022 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2023 return [ 2024 "| | | | | | | |\n", 2025 "| {:<27} | | | | | {:>19} | |\n".format( 2026 noTradeStr if noTradeStr else typeStr, 2027 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2028 ), 2029 ] 2030 2031 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2032 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2033 "{} [{}]".format(data["ticker"], data["figi"]), 2034 "{:.2f} ({:.2f}) {}".format( 2035 data["volume"], 2036 data["blocked"], 2037 data["currency"], 2038 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2039 data["volume"], 2040 data["blocked"], 2041 ), 2042 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2043 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2044 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2045 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2046 "{}{:.2f} {} ({}{:.2f}%)".format( 2047 "+" if data["profit"] > 0 else "", 2048 data["profit"], data["baseCurrencyName"], 2049 "+" if data["percentProfit"] > 0 else "", 2050 data["percentProfit"], 2051 ), 2052 ) 2053 2054 # --- Show currencies section: 2055 if view["stat"]["Currencies"]: 2056 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2057 for item in view["stat"]["Currencies"]: 2058 info.append(_InfoStr(item, showCurrencyName=True)) 2059 2060 else: 2061 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2062 2063 # --- Show shares section: 2064 if view["stat"]["Shares"]: 2065 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2066 2067 for item in view["stat"]["Shares"]: 2068 info.append(_InfoStr(item)) 2069 2070 else: 2071 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2072 2073 # --- Show bonds section: 2074 if view["stat"]["Bonds"]: 2075 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2076 2077 for item in view["stat"]["Bonds"]: 2078 info.append(_InfoStr(item)) 2079 2080 else: 2081 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2082 2083 # --- Show etfs section: 2084 if view["stat"]["Etfs"]: 2085 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2086 2087 for item in view["stat"]["Etfs"]: 2088 info.append(_InfoStr(item)) 2089 2090 else: 2091 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2092 2093 # --- Show futures section: 2094 if view["stat"]["Futures"]: 2095 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2096 2097 for item in view["stat"]["Futures"]: 2098 info.append(_InfoStr(item)) 2099 2100 else: 2101 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2102 2103 if details in ["full", "orders"]: 2104 # --- Show pending orders section: 2105 if view["stat"]["orders"]: 2106 info.extend([ 2107 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2108 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2109 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2110 ]) 2111 2112 for item in view["stat"]["orders"]: 2113 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2114 "{} [{}]".format(item["ticker"], item["figi"]), 2115 item["orderID"], 2116 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2117 "{} {} ({}{:.2f}%)".format( 2118 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2119 item["baseCurrencyName"], 2120 "+" if item["percentChanges"] > 0 else "", 2121 float(item["percentChanges"]), 2122 ), 2123 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2124 item["action"], 2125 item["type"], 2126 item["date"], 2127 )) 2128 2129 else: 2130 info.append("\n## Total pending limit-orders: 0\n") 2131 2132 # --- Show stop orders section: 2133 if view["stat"]["stopOrders"]: 2134 info.extend([ 2135 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2136 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2137 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2138 ]) 2139 2140 for item in view["stat"]["stopOrders"]: 2141 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2142 "{} [{}]".format(item["ticker"], item["figi"]), 2143 item["orderID"], 2144 item["lotsRequested"], 2145 "{} {} ({}{:.2f}%)".format( 2146 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2147 item["baseCurrencyName"], 2148 "+" if item["percentChanges"] > 0 else "", 2149 float(item["percentChanges"]), 2150 ), 2151 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2152 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2153 item["action"], 2154 item["type"], 2155 item["expType"], 2156 item["createDate"], 2157 item["expDate"], 2158 )) 2159 2160 else: 2161 info.append("\n## Total stop-orders: 0\n") 2162 2163 if details in ["full", "analytics"]: 2164 # -- Show analytics section: 2165 if view["stat"]["portfolioCostRUB"] > 0: 2166 info.extend([ 2167 "\n# Analytics\n" 2168 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2169 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2170 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2171 view["stat"]["totalChangesRUB"], 2172 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2173 view["stat"]["totalChangesPercentRUB"], 2174 ), 2175 "\n## Portfolio distribution by assets\n" 2176 "\n| Type | Uniques | Percent | Current cost |\n", 2177 "|------------------------------------|---------|---------|--------------------|\n", 2178 ]) 2179 2180 for key in view["analytics"]["distrByAssets"].keys(): 2181 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2182 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2183 key, 2184 view["analytics"]["distrByAssets"][key]["uniques"], 2185 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2186 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2187 )) 2188 2189 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2190 2191 info.extend([ 2192 "\n## Portfolio distribution by companies\n" 2193 "\n| Company | Percent | Current cost |\n", 2194 aSepLine, 2195 ]) 2196 2197 for company in view["analytics"]["distrByCompanies"].keys(): 2198 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2199 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2200 "{}{}".format( 2201 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2202 company, 2203 ), 2204 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2205 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2206 )) 2207 2208 info.extend([ 2209 "\n## Portfolio distribution by sectors\n" 2210 "\n| Sector | Percent | Current cost |\n", 2211 aSepLine, 2212 ]) 2213 2214 for sector in view["analytics"]["distrBySectors"].keys(): 2215 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2216 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2217 sector, 2218 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2219 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2220 )) 2221 2222 info.extend([ 2223 "\n## Portfolio distribution by currencies\n" 2224 "\n| Instruments currencies | Percent | Current cost |\n", 2225 aSepLine, 2226 ]) 2227 2228 for curr in view["analytics"]["distrByCurrencies"].keys(): 2229 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2230 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2231 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2232 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2233 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2234 )) 2235 2236 info.extend([ 2237 "\n## Portfolio distribution by countries\n" 2238 "\n| Assets by country | Percent | Current cost |\n", 2239 aSepLine, 2240 ]) 2241 2242 for country in view["analytics"]["distrByCountries"].keys(): 2243 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2244 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2245 country, 2246 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2247 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2248 )) 2249 2250 if details in ["full", "calendar"]: 2251 # -- Show bonds payment calendar section: 2252 if view["stat"]["Bonds"]: 2253 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2254 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2255 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2256 2257 else: 2258 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2259 2260 infoText = "".join(info) 2261 2262 uLogger.info(infoText) 2263 2264 if details == "full" and self.overviewFile: 2265 filename = self.overviewFile 2266 2267 elif details == "digest" and self.overviewDigestFile: 2268 filename = self.overviewDigestFile 2269 2270 elif details == "positions" and self.overviewPositionsFile: 2271 filename = self.overviewPositionsFile 2272 2273 elif details == "orders" and self.overviewOrdersFile: 2274 filename = self.overviewOrdersFile 2275 2276 elif details == "analytics" and self.overviewAnalyticsFile: 2277 filename = self.overviewAnalyticsFile 2278 2279 elif details == "calendar" and self.overviewBondsCalendarFile: 2280 filename = self.overviewBondsCalendarFile 2281 2282 else: 2283 filename = "" 2284 2285 if filename: 2286 with open(filename, "w", encoding="UTF-8") as fH: 2287 fH.write(infoText) 2288 2289 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2290 2291 return view 2292 2293 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2294 """ 2295 Returns history operations between two given dates for current `accountId`. 2296 If `reportFile` string is not empty then also save human-readable report. 2297 Shows some statistical data of closed positions. 2298 2299 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2300 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2301 :param show: if `True` then also prints all records to the console. 2302 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2303 :return: original list of dictionaries with history of deals records from API ("operations" key): 2304 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2305 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2306 """ 2307 if self.accountId is None or not self.accountId: 2308 uLogger.error("Variable `accountId` must be defined for using this method!") 2309 raise Exception("Account ID required") 2310 2311 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2312 2313 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2314 2315 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2316 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2317 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2318 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2319 customStat = {} # custom statistics in additional to responseJSON 2320 2321 # --- output report in human-readable format: 2322 if show or self.reportFile: 2323 splitLine1 = "| | | | | |\n" # Summary section 2324 splitLine2 = "| | | | | | | | |\n" # Operations section 2325 nextDay = "" 2326 2327 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2328 2329 if len(ops) > 0: 2330 customStat = { 2331 "opsCount": 0, # total operations count 2332 "buyCount": 0, # buy operations 2333 "sellCount": 0, # sell operations 2334 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2335 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2336 "payIn": {"rub": 0.}, # Deposit brokerage account 2337 "payOut": {"rub": 0.}, # Withdrawals 2338 "divs": {"rub": 0.}, # Dividends income 2339 "coupons": {"rub": 0.}, # Coupon's income 2340 "brokerCom": {"rub": 0.}, # Service commissions 2341 "serviceCom": {"rub": 0.}, # Service commissions 2342 "marginCom": {"rub": 0.}, # Margin commissions 2343 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2344 } 2345 2346 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2347 for item in ops: 2348 if item["state"] == "OPERATION_STATE_EXECUTED": 2349 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2350 2351 # count buy operations: 2352 if "_BUY" in item["operationType"]: 2353 customStat["buyCount"] += 1 2354 2355 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2356 customStat["buyTotal"][item["payment"]["currency"]] += payment 2357 2358 else: 2359 customStat["buyTotal"][item["payment"]["currency"]] = payment 2360 2361 # count sell operations: 2362 elif "_SELL" in item["operationType"]: 2363 customStat["sellCount"] += 1 2364 2365 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2366 customStat["sellTotal"][item["payment"]["currency"]] += payment 2367 2368 else: 2369 customStat["sellTotal"][item["payment"]["currency"]] = payment 2370 2371 # count incoming operations: 2372 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2373 if item["payment"]["currency"] in customStat["payIn"].keys(): 2374 customStat["payIn"][item["payment"]["currency"]] += payment 2375 2376 else: 2377 customStat["payIn"][item["payment"]["currency"]] = payment 2378 2379 # count withdrawals operations: 2380 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2381 if item["payment"]["currency"] in customStat["payOut"].keys(): 2382 customStat["payOut"][item["payment"]["currency"]] += payment 2383 2384 else: 2385 customStat["payOut"][item["payment"]["currency"]] = payment 2386 2387 # count dividends income: 2388 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2389 if item["payment"]["currency"] in customStat["divs"].keys(): 2390 customStat["divs"][item["payment"]["currency"]] += payment 2391 2392 else: 2393 customStat["divs"][item["payment"]["currency"]] = payment 2394 2395 # count coupon's income: 2396 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2397 if item["payment"]["currency"] in customStat["coupons"].keys(): 2398 customStat["coupons"][item["payment"]["currency"]] += payment 2399 2400 else: 2401 customStat["coupons"][item["payment"]["currency"]] = payment 2402 2403 # count broker commissions: 2404 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2405 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2406 customStat["brokerCom"][item["payment"]["currency"]] += payment 2407 2408 else: 2409 customStat["brokerCom"][item["payment"]["currency"]] = payment 2410 2411 # count service commissions: 2412 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2413 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2414 customStat["serviceCom"][item["payment"]["currency"]] += payment 2415 2416 else: 2417 customStat["serviceCom"][item["payment"]["currency"]] = payment 2418 2419 # count margin commissions: 2420 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2421 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2422 customStat["marginCom"][item["payment"]["currency"]] += payment 2423 2424 else: 2425 customStat["marginCom"][item["payment"]["currency"]] = payment 2426 2427 # count withholding taxes: 2428 elif "_TAX" in item["operationType"]: 2429 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2430 customStat["allTaxes"][item["payment"]["currency"]] += payment 2431 2432 else: 2433 customStat["allTaxes"][item["payment"]["currency"]] = payment 2434 2435 else: 2436 continue 2437 2438 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2439 2440 # --- view "Actions" lines: 2441 info.extend([ 2442 "| Report sections | | | | |\n", 2443 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2444 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2445 "| | Buy: {:<22} | {:<28} | | |\n".format( 2446 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2447 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2448 ), 2449 "| | Sell: {:<21} | {:<28} | | |\n".format( 2450 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2451 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2452 ), 2453 ]) 2454 2455 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2456 for key in opsKeys: 2457 if key == "rub": 2458 continue 2459 2460 info.extend([ 2461 "| | | {:<28} | | |\n".format( 2462 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2463 ), 2464 "| | | {:<28} | | |\n".format( 2465 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2466 ), 2467 ]) 2468 2469 info.append(splitLine1) 2470 2471 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2472 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2473 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2474 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2475 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2476 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2477 ) 2478 2479 # --- view "Payments" lines: 2480 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2481 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2482 2483 for key in paymentsKeys: 2484 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2485 2486 info.append(splitLine1) 2487 2488 # --- view "Commissions and taxes" lines: 2489 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2490 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2491 2492 for key in comKeys: 2493 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2494 2495 info.append(splitLine1) 2496 2497 info.extend([ 2498 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2499 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2500 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2501 ]) 2502 2503 else: 2504 info.append("Broker returned no operations during this period\n") 2505 2506 # --- view "Operations" section: 2507 for item in ops: 2508 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2509 continue 2510 2511 else: 2512 self.figi = item["figi"] if item["figi"] else "" 2513 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2514 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2515 2516 # group of deals during one day: 2517 if nextDay and item["date"].split("T")[0] != nextDay: 2518 info.append(splitLine2) 2519 nextDay = "" 2520 2521 else: 2522 nextDay = item["date"].split("T")[0] # saving current day for splitting 2523 2524 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2525 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2526 self.figi if self.figi else "—", 2527 instrument["ticker"] if instrument else "—", 2528 instrument["type"] if instrument else "—", 2529 item["quantity"] if int(item["quantity"]) > 0 else "—", 2530 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2531 TKS_OPERATION_STATES[item["state"]], 2532 TKS_OPERATION_TYPES[item["operationType"]], 2533 )) 2534 2535 infoText = "".join(info) 2536 2537 if show: 2538 if self.moreDebug: 2539 uLogger.debug("Records about history of a client's operations successfully received") 2540 2541 uLogger.info(infoText) 2542 2543 if self.reportFile: 2544 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2545 fH.write(infoText) 2546 2547 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2548 2549 return ops, customStat 2550 2551 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2552 """ 2553 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2554 2555 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2556 Warning! Broker server used ISO UTC time by default. 2557 2558 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2559 Also, `historyFile` used to update history with `onlyMissing` parameter. 2560 2561 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2562 2563 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2564 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2565 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2566 `"hour"`, `"day"`. Default: `"hour"`. 2567 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2568 False by default. Warning! History appends only from last candle to current time 2569 with always update last candle! 2570 :param csvSep: separator if csv-file is used, `,` by default. 2571 :param show: if `True` then also prints Pandas DataFrame to the console. 2572 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2573 `["date", "time", "open", "high", "low", "close", "volume"]`. 2574 """ 2575 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2576 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2577 history = None # empty pandas object for history 2578 2579 if interval not in TKS_CANDLE_INTERVALS.keys(): 2580 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2581 raise Exception("Incorrect value") 2582 2583 if not (self.ticker or self.figi): 2584 uLogger.error("Ticker or FIGI must be defined!") 2585 raise Exception("Ticker or FIGI required") 2586 2587 if self.ticker and not self.figi: 2588 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2589 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2590 2591 if self.figi and not self.ticker: 2592 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2593 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2594 2595 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2596 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2597 if interval.lower() != "day": 2598 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2599 2600 delta = dtEnd - dtStart # current UTC time minus last time in file 2601 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2602 2603 # calculate history length in candles: 2604 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2605 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2606 length += 1 # to avoid fraction time 2607 2608 # calculate data blocks count: 2609 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2610 2611 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2612 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2613 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2614 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2615 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2616 2617 tempOld = None # pandas object for old history, if --only-missing key present 2618 lastTime = None # datetime object of last old candle in file 2619 2620 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2621 uLogger.debug("--only-missing key present, add only last missing candles...") 2622 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2623 2624 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2625 2626 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2627 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2628 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2629 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2630 2631 # get last datetime object from last string in file or minus 1 delta if file is empty: 2632 if len(tempOld) > 0: 2633 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2634 2635 else: 2636 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2637 2638 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2639 2640 responseJSONs = [] # raw history blocks of data 2641 2642 blockEnd = dtEnd 2643 for item in range(blocks): 2644 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2645 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2646 2647 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2648 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2649 )) 2650 2651 if blockStart == blockEnd: 2652 uLogger.debug("Skipped this zero-length block...") 2653 2654 else: 2655 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2656 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2657 self.body = str({ 2658 "figi": self.figi, 2659 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2660 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2661 "interval": TKS_CANDLE_INTERVALS[interval][0] 2662 }) 2663 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2664 2665 if "code" in responseJSON.keys(): 2666 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2667 2668 else: 2669 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2670 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2671 2672 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2673 2674 blockEnd = blockStart 2675 2676 printCount = len(responseJSONs) # candles to show in console 2677 if responseJSONs: 2678 tempHistory = pd.DataFrame( 2679 data={ 2680 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2681 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2682 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2683 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2684 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2685 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2686 "volume": [int(item["volume"]) for item in responseJSONs], 2687 }, 2688 index=range(len(responseJSONs)), 2689 columns=["date", "time", "open", "high", "low", "close", "volume"], 2690 ) 2691 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2692 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2693 2694 # append only newest candles to old history if --only-missing key present: 2695 if onlyMissing and tempOld is not None and lastTime is not None: 2696 index = 0 # find start index in tempHistory data: 2697 2698 for i, item in tempHistory.iterrows(): 2699 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2700 2701 if curTime == lastTime: 2702 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2703 index = i 2704 printCount = index + 1 2705 break 2706 2707 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2708 2709 else: 2710 history = tempHistory # if no `--only-missing` key then load full data from server 2711 2712 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2713 2714 if history is not None and not history.empty: 2715 if show: 2716 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2717 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2718 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2719 )) 2720 2721 else: 2722 uLogger.warning("Received an empty candles history!") 2723 2724 if self.historyFile is not None: 2725 if history is not None and not history.empty: 2726 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2727 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2728 2729 else: 2730 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2731 2732 else: 2733 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2734 2735 return history 2736 2737 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2738 """ 2739 Load candles history from csv-file and return Pandas DataFrame object. 2740 2741 See also: `History()` and `ShowHistoryChart()` methods. 2742 2743 :param filePath: path to csv-file to open. 2744 """ 2745 loadedHistory = None # init candles data object 2746 2747 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2748 2749 if os.path.exists(filePath): 2750 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2751 2752 tfStr = self.priceModel.FormattedDelta( 2753 self.priceModel.timeframe, 2754 "{days} days {hours}h {minutes}m {seconds}s", 2755 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2756 self.priceModel.timeframe, 2757 "{hours}h {minutes}m {seconds}s", 2758 ) 2759 2760 if loadedHistory is not None and not loadedHistory.empty: 2761 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2762 len(loadedHistory), 2763 tfStr, 2764 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2765 ) 2766 2767 else: 2768 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2769 2770 else: 2771 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2772 2773 return loadedHistory 2774 2775 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2776 """ 2777 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2778 2779 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2780 Default: `index.html` (both for interact and non-interact candlesticks chart). 2781 2782 See also: `History()` and `LoadHistory()` methods. 2783 2784 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2785 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2786 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2787 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2788 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2789 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2790 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2791 """ 2792 if isinstance(candles, str): 2793 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2794 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2795 2796 elif isinstance(candles, pd.DataFrame): 2797 self.priceModel.prices = candles # set candles chain from variable 2798 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2799 2800 if "datetime" not in candles.columns: 2801 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2802 2803 else: 2804 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2805 raise Exception("Incorrect value") 2806 2807 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2808 2809 if interact: 2810 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2811 2812 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2813 2814 else: 2815 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2816 2817 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2818 2819 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2820 2821 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2822 """ 2823 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2824 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2825 2826 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2827 2828 :param operation: string "Buy" or "Sell". 2829 :param lots: volume, integer count of lots >= 1. 2830 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2831 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2832 :param expDate: string "Undefined" by default or local date in future, 2833 it is a string with format `%Y-%m-%d %H:%M:%S`. 2834 :return: JSON with response from broker server. 2835 """ 2836 if self.accountId is None or not self.accountId: 2837 uLogger.error("Variable `accountId` must be defined for using this method!") 2838 raise Exception("Account ID required") 2839 2840 if operation is None or not operation or operation not in ("Buy", "Sell"): 2841 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2842 raise Exception("Incorrect value") 2843 2844 if lots is None or lots < 1: 2845 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2846 lots = 1 2847 2848 if tp is None or tp < 0: 2849 tp = 0 2850 2851 if sl is None or sl < 0: 2852 sl = 0 2853 2854 if expDate is None or not expDate: 2855 expDate = "Undefined" 2856 2857 if not (self.ticker or self.figi): 2858 uLogger.error("Ticker or FIGI must be defined!") 2859 raise Exception("Ticker or FIGI required") 2860 2861 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 2862 self.ticker = instrument["ticker"] 2863 self.figi = instrument["figi"] 2864 2865 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2866 2867 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2868 self.body = str({ 2869 "figi": self.figi, 2870 "quantity": str(lots), 2871 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2872 "accountId": str(self.accountId), 2873 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2874 }) 2875 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2876 2877 if "orderId" in response.keys(): 2878 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2879 operation, response["orderId"], 2880 self.ticker, self.figi, lots, 2881 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2882 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2883 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2884 )) 2885 2886 if tp > 0: 2887 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2888 2889 if sl > 0: 2890 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2891 2892 else: 2893 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.") 2894 2895 return response 2896 2897 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2898 """ 2899 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2900 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2901 2902 See also: `Order()` and `Trade()` docstrings. 2903 2904 :param lots: volume, integer count of lots >= 1. 2905 :param tp: float > 0, take profit price of stop-order. 2906 :param sl: float > 0, stop loss price of stop-order. 2907 :param expDate: it's a local date in future. 2908 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2909 :return: JSON with response from broker server. 2910 """ 2911 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 2912 2913 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2914 """ 2915 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2916 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2917 2918 See also: `Order()` and `Trade()` docstrings. 2919 2920 :param lots: volume, integer count of lots >= 1. 2921 :param tp: float > 0, take profit price of stop-order. 2922 :param sl: float > 0, stop loss price of stop-order. 2923 :param expDate: it's a local date in the future. 2924 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2925 :return: JSON with response from broker server. 2926 """ 2927 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 2928 2929 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 2930 """ 2931 Close position of given instruments. 2932 2933 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 2934 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 2935 This avoids unnecessary downloading data from the server. 2936 """ 2937 if instruments is None or not instruments: 2938 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 2939 raise Exception("Ticker or FIGI required") 2940 2941 if isinstance(instruments, str): 2942 instruments = [instruments] 2943 2944 uniqueInstruments = self.GetUniqueFIGIs(instruments) 2945 if uniqueInstruments: 2946 if portfolio is None or not portfolio: 2947 portfolio = self.Overview(show=False) 2948 2949 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 2950 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 2951 2952 for self.figi in uniqueInstruments: 2953 if self.figi not in allOpened: 2954 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi)) 2955 continue 2956 2957 # search open trade info about instrument by ticker: 2958 instrument = {} 2959 for iType in TKS_INSTRUMENTS: 2960 if instrument: 2961 break 2962 2963 for item in portfolio["stat"][iType]: 2964 if item["figi"] == self.figi: 2965 instrument = item 2966 break 2967 2968 if instrument: 2969 self.ticker = instrument["ticker"] 2970 self.figi = instrument["figi"] 2971 2972 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 2973 self.ticker, 2974 self.figi, 2975 int(instrument["volume"]), 2976 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 2977 )) 2978 2979 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 2980 2981 if tradeLots > 0: 2982 if instrument["blocked"] > 0: 2983 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 2984 instrument["blocked"], 2985 self.ticker, 2986 tradeLots, 2987 )) 2988 2989 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 2990 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 2991 2992 else: 2993 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 2994 2995 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 2996 """ 2997 Close all positions of given instruments with defined type. 2998 2999 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3000 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3001 This avoids unnecessary downloading data from the server. 3002 """ 3003 if iType not in TKS_INSTRUMENTS: 3004 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3005 3006 else: 3007 if portfolio is None or not portfolio: 3008 portfolio = self.Overview(show=False) 3009 3010 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3011 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3012 3013 if tickers and portfolio: 3014 self.CloseTrades(tickers, portfolio) 3015 3016 else: 3017 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3018 3019 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3020 """ 3021 Universal method to create market or limit orders with all available parameters for current `accountId`. 3022 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3023 3024 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3025 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3026 3027 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3028 then broker immediately open market order as you can do simple --buy or --sell operations! 3029 3030 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3031 When current price will go up or down to target price value then broker opens a limit order. 3032 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3033 3034 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3035 3036 :param operation: string "Buy" or "Sell". 3037 :param orderType: string "Limit" or "Stop". 3038 :param lots: volume, integer count of lots >= 1. 3039 :param targetPrice: target price > 0. This is open trade price for limit order. 3040 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3041 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3042 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3043 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3044 Stop loss order always executed by market price. 3045 :param expDate: string "Undefined" by default or local date in future. 3046 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3047 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3048 A limit order has no expiration date, it lasts until the end of the trading day. 3049 :return: JSON with response from broker server. 3050 """ 3051 if self.accountId is None or not self.accountId: 3052 uLogger.error("Variable `accountId` must be defined for using this method!") 3053 raise Exception("Account ID required") 3054 3055 if operation is None or not operation or operation not in ("Buy", "Sell"): 3056 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3057 raise Exception("Incorrect value") 3058 3059 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3060 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3061 raise Exception("Incorrect value") 3062 3063 if lots is None or lots < 1: 3064 uLogger.error("You must define trade volume > 0: integer count of lots!") 3065 raise Exception("Incorrect value") 3066 3067 if targetPrice is None or targetPrice <= 0: 3068 uLogger.error("Target price for limit-order must be greater than 0!") 3069 raise Exception("Incorrect value") 3070 3071 if limitPrice is None or limitPrice <= 0: 3072 limitPrice = targetPrice 3073 3074 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3075 stopType = "Limit" 3076 3077 if expDate is None or not expDate: 3078 expDate = "Undefined" 3079 3080 if not (self.ticker or self.figi): 3081 uLogger.error("Tocker or FIGI must be defined!") 3082 raise Exception("Ticker or FIGI required") 3083 3084 response = {} 3085 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 3086 self.ticker = instrument["ticker"] 3087 self.figi = instrument["figi"] 3088 3089 if orderType == "Limit": 3090 uLogger.debug( 3091 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3092 self.ticker, self.figi, 3093 operation, lots, targetPrice, instrument["currency"], 3094 )) 3095 3096 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3097 self.body = str({ 3098 "figi": self.figi, 3099 "quantity": str(lots), 3100 "price": FloatToNano(targetPrice), 3101 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3102 "accountId": str(self.accountId), 3103 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3104 }) 3105 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3106 3107 if "orderId" in response.keys(): 3108 uLogger.info( 3109 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3110 response["orderId"], 3111 self.ticker, self.figi, 3112 operation, lots, targetPrice, instrument["currency"], 3113 )) 3114 3115 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3116 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3117 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3118 targetPrice, instrument["currency"], 3119 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3120 )) 3121 3122 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3123 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3124 targetPrice, instrument["currency"], 3125 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3126 )) 3127 3128 else: 3129 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3130 3131 if orderType == "Stop": 3132 uLogger.debug( 3133 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3134 self.ticker, self.figi, 3135 operation, lots, 3136 targetPrice, instrument["currency"], 3137 limitPrice, instrument["currency"], 3138 stopType, expDate, 3139 )) 3140 3141 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3142 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3143 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3144 3145 body = { 3146 "figi": self.figi, 3147 "quantity": str(lots), 3148 "price": FloatToNano(limitPrice), 3149 "stopPrice": FloatToNano(targetPrice), 3150 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3151 "accountId": str(self.accountId), 3152 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3153 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3154 } 3155 3156 if expDateUTC: 3157 body["expireDate"] = expDateUTC 3158 3159 self.body = str(body) 3160 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3161 3162 if "stopOrderId" in response.keys(): 3163 uLogger.info( 3164 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3165 response["stopOrderId"], 3166 self.ticker, self.figi, 3167 operation, lots, 3168 targetPrice, instrument["currency"], 3169 limitPrice, instrument["currency"], 3170 TKS_STOP_ORDER_TYPES[stopOrderType], 3171 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3172 )) 3173 3174 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3175 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3176 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3177 targetPrice, instrument["currency"], 3178 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3179 )) 3180 3181 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3182 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3183 targetPrice, instrument["currency"], 3184 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3185 )) 3186 3187 else: 3188 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3189 3190 return response 3191 3192 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3193 """ 3194 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3195 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3196 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3197 See also: `Order()` docstring. 3198 3199 :param lots: volume, integer count of lots >= 1. 3200 :param targetPrice: target price > 0. This is open trade price for limit order. 3201 :return: JSON with response from broker server. 3202 """ 3203 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3204 3205 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3206 """ 3207 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3208 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3209 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3210 target price value then broker opens a limit order. See also: `Order()` docstring. 3211 3212 :param lots: volume, integer count of lots >= 1. 3213 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3214 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3215 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3216 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3217 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3218 :param expDate: string "Undefined" by default or local date in future. 3219 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3220 This date is converting to UTC format for server. 3221 :return: JSON with response from broker server. 3222 """ 3223 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3224 3225 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3226 """ 3227 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3228 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3229 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3230 See also: `Order()` docstring. 3231 3232 :param lots: volume, integer count of lots >= 1. 3233 :param targetPrice: target price > 0. This is open trade price for limit order. 3234 :return: JSON with response from broker server. 3235 """ 3236 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3237 3238 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3239 """ 3240 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3241 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3242 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3243 target price value then broker opens a limit order. See also: `Order()` docstring. 3244 3245 :param lots: volume, integer count of lots >= 1. 3246 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3247 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3248 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3249 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3250 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3251 :param expDate: string "Undefined" by default or local date in future. 3252 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3253 This date is converting to UTC format for server. 3254 :return: JSON with response from broker server. 3255 """ 3256 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3257 3258 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3259 """ 3260 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3261 3262 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3263 :param allOrdersIDs: pre-received lists of all active pending orders. 3264 This avoids unnecessary downloading data from the server. 3265 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3266 """ 3267 if self.accountId is None or not self.accountId: 3268 uLogger.error("Variable `accountId` must be defined for using this method!") 3269 raise Exception("Account ID required") 3270 3271 if orderIDs: 3272 if allOrdersIDs is None or not allOrdersIDs: 3273 rawOrders = self.RequestPendingOrders() 3274 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3275 3276 if allStopOrdersIDs is None or not allStopOrdersIDs: 3277 rawStopOrders = self.RequestStopOrders() 3278 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3279 3280 for orderID in orderIDs: 3281 idInPendingOrders = orderID in allOrdersIDs 3282 idInStopOrders = orderID in allStopOrdersIDs 3283 3284 if not (idInPendingOrders or idInStopOrders): 3285 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3286 continue 3287 3288 else: 3289 if idInPendingOrders: 3290 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3291 3292 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3293 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3294 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3295 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3296 3297 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3298 if self.moreDebug: 3299 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3300 3301 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3302 3303 else: 3304 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3305 3306 elif idInStopOrders: 3307 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3308 3309 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3310 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3311 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3312 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3313 3314 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3315 if self.moreDebug: 3316 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3317 3318 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3319 3320 else: 3321 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3322 3323 else: 3324 continue 3325 3326 def CloseAllOrders(self) -> None: 3327 """ 3328 Gets a list of open pending and stop orders and cancel it all. 3329 """ 3330 rawOrders = self.RequestPendingOrders() 3331 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3332 lenOrders = len(allOrdersIDs) 3333 3334 rawStopOrders = self.RequestStopOrders() 3335 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3336 lenSOrders = len(allStopOrdersIDs) 3337 3338 if lenOrders > 0 or lenSOrders > 0: 3339 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3340 3341 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3342 3343 else: 3344 uLogger.info("Orders not found, nothing to cancel.") 3345 3346 def CloseAll(self, *args) -> None: 3347 """ 3348 Close all available (not blocked) opened trades and orders. 3349 3350 Also, you can select one or more keywords case-insensitive: 3351 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3352 3353 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3354 """ 3355 overview = self.Overview(show=False) # get all open trades info 3356 3357 if len(args) == 0: 3358 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3359 self.CloseAllOrders() # close all pending and stop orders 3360 3361 for iType in TKS_INSTRUMENTS: 3362 if iType != "Currencies": 3363 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3364 3365 else: 3366 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3367 lowerArgs = [x.lower() for x in args] 3368 3369 if "orders" in lowerArgs: 3370 self.CloseAllOrders() # close all pending and stop orders 3371 3372 for iType in TKS_INSTRUMENTS: 3373 if iType.lower() in lowerArgs and iType != "Currencies": 3374 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3375 3376 @staticmethod 3377 def ParseOrderParameters(operation, **inputParameters): 3378 """ 3379 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3380 3381 :param operation: string "Buy" or "Sell". 3382 :param inputParameters: this is dict of strings that looks like this 3383 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3384 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3385 "prices" key: one or more prices to open limit-orders 3386 Counts of values in lots and prices lists must be equals! 3387 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3388 """ 3389 # TODO: update order grid work with api v2 3390 pass 3391 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3392 # 3393 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3394 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3395 # raise Exception("Incorrect value") 3396 # 3397 # if "l" in inputParameters.keys(): 3398 # inputParameters["lots"] = inputParameters.pop("l") 3399 # 3400 # if "p" in inputParameters.keys(): 3401 # inputParameters["prices"] = inputParameters.pop("p") 3402 # 3403 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3404 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3405 # raise Exception("Incorrect value") 3406 # 3407 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3408 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3409 # 3410 # if len(lots) != len(prices): 3411 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3412 # raise Exception("Incorrect value") 3413 # 3414 # uLogger.debug("Extracted parameters for orders:") 3415 # uLogger.debug("lots = {}".format(lots)) 3416 # uLogger.debug("prices = {}".format(prices)) 3417 # 3418 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3419 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3420 # uLogger.debug("Order parameters: {}".format(result)) 3421 # 3422 # return result 3423 3424 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3425 """ 3426 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3427 3428 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3429 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3430 """ 3431 result = False 3432 msg = "Instrument not defined!" 3433 3434 if portfolio is None or not portfolio: 3435 portfolio = self.Overview(show=False) 3436 3437 if self.ticker: 3438 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3439 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3440 3441 for iType in TKS_INSTRUMENTS: 3442 for instrument in portfolio["stat"][iType]: 3443 if instrument["ticker"] == self.ticker: 3444 result = True 3445 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3446 break 3447 3448 elif self.figi: 3449 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3450 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3451 3452 for iType in TKS_INSTRUMENTS: 3453 for instrument in portfolio["stat"][iType]: 3454 if instrument["figi"] == self.figi: 3455 result = True 3456 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3457 break 3458 3459 else: 3460 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3461 3462 uLogger.debug(msg) 3463 3464 return result 3465 3466 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3467 """ 3468 Returns instrument from the user's portfolio if it presents there. 3469 Instrument must be defined by `ticker` (highly priority) or `figi`. 3470 3471 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3472 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3473 """ 3474 result = None 3475 msg = "Instrument not defined!" 3476 3477 if portfolio is None or not portfolio: 3478 portfolio = self.Overview(show=False) 3479 3480 if self.ticker: 3481 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker)) 3482 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3483 3484 for iType in TKS_INSTRUMENTS: 3485 for instrument in portfolio["stat"][iType]: 3486 if instrument["ticker"] == self.ticker: 3487 result = instrument 3488 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3489 break 3490 3491 elif self.figi: 3492 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3493 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3494 3495 for iType in TKS_INSTRUMENTS: 3496 for instrument in portfolio["stat"][iType]: 3497 if instrument["figi"] == self.figi: 3498 result = instrument 3499 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3500 break 3501 3502 else: 3503 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3504 3505 uLogger.debug(msg) 3506 3507 return result 3508 3509 def RequestLimits(self) -> dict: 3510 """ 3511 Method for obtaining the available funds for withdrawal for current `accountId`. 3512 3513 See also: 3514 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3515 - `OverviewLimits()` method 3516 3517 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3518 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3519 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3520 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3521 """ 3522 if self.accountId is None or not self.accountId: 3523 uLogger.error("Variable `accountId` must be defined for using this method!") 3524 raise Exception("Account ID required") 3525 3526 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3527 3528 self.body = str({"accountId": self.accountId}) 3529 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3530 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3531 3532 if self.moreDebug: 3533 uLogger.debug("Records about available funds for withdrawal successfully received") 3534 3535 return rawLimits 3536 3537 def OverviewLimits(self, show: bool = False) -> dict: 3538 """ 3539 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3540 3541 See also: `RequestLimits()`. 3542 3543 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3544 :return: dict with raw parsed data from server and some calculated statistics about it. 3545 """ 3546 if self.accountId is None or not self.accountId: 3547 uLogger.error("Variable `accountId` must be defined for using this method!") 3548 raise Exception("Account ID required") 3549 3550 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3551 3552 view = { 3553 "rawLimits": rawLimits, 3554 "limits": { # parsed data for every currency: 3555 "money": { # this is an array of portfolio currency positions 3556 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3557 }, 3558 "blocked": { # this is an array of blocked currency 3559 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3560 }, 3561 "blockedGuarantee": { # this is locked money under collateral for futures 3562 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3563 }, 3564 }, 3565 } 3566 3567 # --- Prepare text table with limits in human-readable format: 3568 if show: 3569 info = [ 3570 "# Withdrawal limits\n\n", 3571 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3572 "* **Account ID:** [{}]\n".format(self.accountId), 3573 ] 3574 3575 if view["limits"]["money"]: 3576 info.extend([ 3577 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3578 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3579 ]) 3580 3581 else: 3582 info.append("\nNo withdrawal limits\n") 3583 3584 for curr in view["limits"]["money"].keys(): 3585 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3586 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3587 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3588 3589 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3590 "[{}]".format(curr), 3591 "{:.2f}".format(view["limits"]["money"][curr]), 3592 "{:.2f}".format(availableMoney), 3593 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3594 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3595 ) 3596 3597 if curr == "rub": 3598 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3599 3600 else: 3601 info.append(infoStr) 3602 3603 infoText = "".join(info) 3604 3605 uLogger.info(infoText) 3606 3607 if self.withdrawalLimitsFile: 3608 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3609 fH.write(infoText) 3610 3611 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3612 3613 return view 3614 3615 def RequestAccounts(self) -> dict: 3616 """ 3617 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3618 3619 See also: 3620 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3621 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3622 - `OverviewUserInfo()` method 3623 3624 :return: dict with raw data from server that contains accounts info. Example of dict: 3625 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3626 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3627 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3628 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3629 """ 3630 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3631 3632 self.body = str({}) 3633 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3634 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3635 3636 if self.moreDebug: 3637 uLogger.debug("Records about available accounts successfully received") 3638 3639 return rawAccounts 3640 3641 def RequestUserInfo(self) -> dict: 3642 """ 3643 Method for requesting common user's information. 3644 3645 See also: 3646 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3647 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3648 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3649 - `OverviewUserInfo()` method 3650 3651 :return: dict with raw data from server that contains user's information. Example of dict: 3652 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3653 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3654 """ 3655 uLogger.debug("Requesting common user's information. Wait, please...") 3656 3657 self.body = str({}) 3658 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3659 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3660 3661 if self.moreDebug: 3662 uLogger.debug("Records about current user successfully received") 3663 3664 return rawUserInfo 3665 3666 def RequestMarginStatus(self, accountId: str = None) -> dict: 3667 """ 3668 Method for requesting margin calculation for defined account ID. 3669 3670 See also: 3671 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3672 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3673 - `OverviewUserInfo()` method 3674 3675 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3676 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3677 Example of responses: 3678 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3679 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3680 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3681 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3682 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3683 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3684 """ 3685 if accountId is None or not accountId: 3686 if self.accountId is None or not self.accountId: 3687 uLogger.error("Variable `accountId` must be defined for using this method!") 3688 raise Exception("Account ID required") 3689 3690 else: 3691 accountId = self.accountId # use `self.accountId` (main ID) by default 3692 3693 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3694 3695 self.body = str({"accountId": accountId}) 3696 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3697 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3698 3699 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3700 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3701 rawMargin = {} 3702 3703 else: 3704 if self.moreDebug: 3705 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3706 3707 return rawMargin 3708 3709 def RequestTariffLimits(self) -> dict: 3710 """ 3711 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3712 3713 See also: 3714 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3715 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3716 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3717 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3718 - `OverviewUserInfo()` method 3719 3720 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3721 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3722 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3723 """ 3724 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3725 3726 self.body = str({}) 3727 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3728 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3729 3730 if self.moreDebug: 3731 uLogger.debug("Records with limits of current tariff successfully received") 3732 3733 return rawTariffLimits 3734 3735 def RequestBondCoupons(self, iJSON: dict) -> dict: 3736 """ 3737 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3738 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3739 All dates are in UTC timezone. 3740 3741 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3742 Documentation: 3743 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3744 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3745 3746 See also: `ExtendBondsData()`. 3747 3748 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3749 If raw iJSON is not data of bond then server returns an error [400] with message: 3750 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3751 :return: dictionary with bond payment calendar. Response example 3752 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3753 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3754 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3755 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3756 """ 3757 if iJSON["figi"] is None or not iJSON["figi"]: 3758 uLogger.error("FIGI must be defined for using this method!") 3759 raise Exception("FIGI required") 3760 3761 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3762 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3763 3764 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3765 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3766 self.figi, 3767 startDate, 3768 endDate, 3769 )) 3770 3771 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3772 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3773 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 3774 3775 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3776 uLogger.warning("Instrument type is not bond!") 3777 3778 else: 3779 if self.moreDebug: 3780 uLogger.debug("Records about bond payment calendar successfully received") 3781 3782 return calendar 3783 3784 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3785 """ 3786 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3787 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3788 coupon yields, current yields and some statistics etc. 3789 3790 WARNING! This is too long operation if a lot of bonds requested from broker server. 3791 3792 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3793 3794 :param instruments: list of strings with tickers or FIGIs. 3795 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3796 for further used by data scientists or stock analytics. 3797 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3798 In XLSX-file and Pandas DataFrame fields mean: 3799 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3800 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3801 """ 3802 if instruments is None or not instruments: 3803 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3804 raise Exception("Ticker or FIGI required") 3805 3806 if isinstance(instruments, str): 3807 instruments = [instruments] 3808 3809 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3810 3811 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3812 3813 iCount = len(uniqueInstruments) 3814 tooLong = iCount >= 20 3815 if tooLong: 3816 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3817 3818 bonds = None 3819 for i, self.figi in enumerate(uniqueInstruments): 3820 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3821 3822 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3823 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3824 rawBond = self.SearchByFIGI(requestPrice=True) 3825 3826 # Widen raw data with UTC current time (iData["actualDateTime"]): 3827 actualDate = datetime.now(tzutc()) 3828 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3829 3830 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3831 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3832 3833 # Replace some values with human-readable: 3834 iData["nominalCurrency"] = iData["nominal"]["currency"] 3835 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3836 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3837 iData["aciCurrency"] = iData["aciValue"]["currency"] 3838 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3839 iData["issueSize"] = int(iData["issueSize"]) 3840 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3841 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3842 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3843 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3844 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3845 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3846 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3847 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3848 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3849 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3850 3851 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3852 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3853 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3854 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3855 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3856 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3857 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3858 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3859 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3860 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3861 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3862 3863 # Widen raw data with calendar data from `rawCalendar` values: 3864 calendarData = [] 3865 if "events" in iData["rawCalendar"].keys(): 3866 for item in iData["rawCalendar"]["events"]: 3867 calendarData.append({ 3868 "couponDate": item["couponDate"], 3869 "couponNumber": int(item["couponNumber"]), 3870 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3871 "payCurrency": item["payOneBond"]["currency"], 3872 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3873 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3874 "couponStartDate": item["couponStartDate"], 3875 "couponEndDate": item["couponEndDate"], 3876 "couponPeriod": item["couponPeriod"], 3877 }) 3878 3879 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3880 if "maturityDate" not in iData.keys(): 3881 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3882 3883 # Widen raw data with Coupon Rate. 3884 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3885 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3886 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3887 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3888 3889 # Widen raw data with Yield to Maturity (YTM) on current date. 3890 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3891 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3892 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3893 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3894 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3895 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3896 3897 iData["calendar"] = calendarData # adds calendar at the end 3898 3899 # Remove not used data: 3900 iData.pop("uid") 3901 iData.pop("positionUid") 3902 iData.pop("currentPrice") 3903 iData.pop("rawCalendar") 3904 3905 colNames = list(iData.keys()) 3906 if bonds is None: 3907 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3908 3909 else: 3910 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3911 3912 else: 3913 uLogger.warning("Instrument is not a bond!") 3914 3915 processed = round(100 * (i + 1) / iCount, 1) 3916 if tooLong and processed % 5 == 0: 3917 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3918 3919 else: 3920 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3921 3922 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3923 3924 # Saving bonds from Pandas DataFrame to XLSX sheet: 3925 if xlsx and self.bondsXLSXFile: 3926 with pd.ExcelWriter( 3927 path=self.bondsXLSXFile, 3928 date_format=TKS_DATE_FORMAT, 3929 datetime_format=TKS_DATE_TIME_FORMAT, 3930 mode="w", 3931 ) as writer: 3932 bonds.to_excel( 3933 writer, 3934 sheet_name="Extended bonds data", 3935 index=True, 3936 encoding="UTF-8", 3937 freeze_panes=(1, 1), 3938 ) # saving as XLSX-file with freeze first row and column as headers 3939 3940 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 3941 3942 return bonds 3943 3944 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 3945 """ 3946 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 3947 3948 WARNING! This is too long operation if a lot of bonds requested from broker server. 3949 3950 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 3951 3952 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 3953 extended information about bonds: main info, current prices, bond payment calendar, 3954 coupon yields, current yields and some statistics etc. 3955 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 3956 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 3957 for further used by data scientists or stock analytics. 3958 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 3959 """ 3960 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 3961 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 3962 3963 uLogger.debug("Generating bond payments calendar data. Wait, please...") 3964 3965 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 3966 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 3967 calendar = None 3968 for bond in extBonds.iterrows(): 3969 for item in bond[1]["calendar"]: 3970 cData = { 3971 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 3972 "couponDate": item["couponDate"], 3973 "figi": bond[1]["figi"], 3974 "ticker": bond[1]["ticker"], 3975 "name": bond[1]["name"], 3976 "couponNumber": item["couponNumber"], 3977 "payOneBond": item["payOneBond"], 3978 "payCurrency": item["payCurrency"], 3979 "couponType": item["couponType"], 3980 "couponPeriod": item["couponPeriod"], 3981 "fixDate": item["fixDate"], 3982 "couponStartDate": item["couponStartDate"], 3983 "couponEndDate": item["couponEndDate"], 3984 } 3985 3986 if calendar is None: 3987 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 3988 3989 else: 3990 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 3991 3992 if calendar is not None: 3993 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 3994 3995 # Saving calendar from Pandas DataFrame to XLSX sheet: 3996 if xlsx: 3997 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 3998 3999 with pd.ExcelWriter( 4000 path=xlsxCalendarFile, 4001 date_format=TKS_DATE_FORMAT, 4002 datetime_format=TKS_DATE_TIME_FORMAT, 4003 mode="w", 4004 ) as writer: 4005 humanReadable = calendar.copy(deep=True) 4006 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4007 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4008 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4009 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4010 humanReadable.columns = colNames # human-readable column names 4011 4012 humanReadable.to_excel( 4013 writer, 4014 sheet_name="Bond payments calendar", 4015 index=False, 4016 encoding="UTF-8", 4017 freeze_panes=(1, 2), 4018 ) # saving as XLSX-file with freeze first row and column as headers 4019 4020 del humanReadable # release df in memory 4021 4022 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4023 4024 return calendar 4025 4026 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4027 """ 4028 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4029 Also, creates Markdown file with calendar data, `calendar.md` by default. 4030 4031 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4032 4033 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4034 extended information about bonds: main info, current prices, bond payment calendar, 4035 coupon yields, current yields and some statistics etc. 4036 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4037 :param show: if `True` then also printing bonds payment calendar to the console, 4038 otherwise save to file `calendarFile` only. `False` by default. 4039 :return: multilines text in Markdown format with bonds payment calendar as a table. 4040 """ 4041 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4042 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4043 4044 infoText = "# Bond payments calendar\n\n" 4045 4046 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4047 4048 if not (calendar is None or calendar.empty): 4049 splitLine = "| | | | | | | | | |\n" 4050 4051 info = [ 4052 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4053 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4054 ] 4055 4056 newMonth = False 4057 notOneBond = calendar["figi"].nunique() > 1 4058 for i, bond in enumerate(calendar.iterrows()): 4059 if newMonth and notOneBond: 4060 info.append(splitLine) 4061 4062 info.append( 4063 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4064 " √" if bond[1]["paid"] else " —", 4065 bond[1]["couponDate"].split("T")[0], 4066 bond[1]["figi"], 4067 bond[1]["ticker"], 4068 bond[1]["couponNumber"], 4069 "{} {}".format( 4070 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4071 bond[1]["payCurrency"], 4072 ), 4073 bond[1]["couponType"], 4074 bond[1]["couponPeriod"], 4075 bond[1]["fixDate"].split("T")[0], 4076 ) 4077 ) 4078 4079 if i < len(calendar.values) - 1: 4080 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4081 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4082 newMonth = False if curDate.month == nextDate.month else True 4083 4084 else: 4085 newMonth = False 4086 4087 infoText += "".join(info) 4088 4089 if show: 4090 uLogger.info("{}".format(infoText)) 4091 4092 if self.calendarFile is not None: 4093 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4094 fH.write(infoText) 4095 4096 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4097 4098 else: 4099 infoText += "No data\n" 4100 4101 return infoText 4102 4103 def OverviewAccounts(self, show: bool = False) -> dict: 4104 """ 4105 Method for parsing and show simple table with all available user accounts. 4106 4107 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4108 4109 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4110 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4111 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4112 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4113 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4114 "closed": "—", "access": "Full access" }, ...}}` 4115 """ 4116 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4117 4118 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4119 accounts = { 4120 item["id"]: { 4121 "type": TKS_ACCOUNT_TYPES[item["type"]], 4122 "name": item["name"], 4123 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4124 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4125 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4126 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4127 } for item in rawAccounts["accounts"] 4128 } 4129 4130 # Raw and parsed data with some fields replaced in "stat" section: 4131 view = { 4132 "rawAccounts": rawAccounts, 4133 "stat": accounts, 4134 } 4135 4136 # --- Prepare simple text table with only accounts data in human-readable format: 4137 if show: 4138 info = [ 4139 "# User accounts\n\n", 4140 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4141 "| Account ID | Type | Status | Name |\n", 4142 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4143 ] 4144 4145 for account in view["stat"].keys(): 4146 info.extend([ 4147 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4148 account, 4149 view["stat"][account]["type"], 4150 view["stat"][account]["status"], 4151 view["stat"][account]["name"], 4152 ) 4153 ]) 4154 4155 infoText = "".join(info) 4156 4157 uLogger.info(infoText) 4158 4159 if self.userAccountsFile: 4160 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4161 fH.write(infoText) 4162 4163 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4164 4165 return view 4166 4167 def OverviewUserInfo(self, show: bool = False) -> dict: 4168 """ 4169 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4170 4171 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4172 4173 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4174 :return: dict with raw parsed data from server and some calculated statistics about it. 4175 """ 4176 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4177 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4178 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4179 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4180 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4181 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4182 4183 # This is dict with parsed common user data: 4184 userInfo = { 4185 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4186 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4187 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4188 "tariff": rawUserInfo["tariff"], 4189 } 4190 4191 # This is an array of dict with parsed margin statuses for every account IDs: 4192 margins = {} 4193 for accountId in accounts.keys(): 4194 if rawMargins[accountId]: 4195 margins[accountId] = { 4196 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4197 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4198 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4199 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4200 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4201 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4202 } 4203 4204 else: 4205 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4206 4207 unary = {} # unary-connection limits 4208 for item in rawTariffLimits["unaryLimits"]: 4209 if item["limitPerMinute"] in unary.keys(): 4210 unary[item["limitPerMinute"]].extend(item["methods"]) 4211 4212 else: 4213 unary[item["limitPerMinute"]] = item["methods"] 4214 4215 stream = {} # stream-connection limits 4216 for item in rawTariffLimits["streamLimits"]: 4217 if item["limit"] in stream.keys(): 4218 stream[item["limit"]].extend(item["streams"]) 4219 4220 else: 4221 stream[item["limit"]] = item["streams"] 4222 4223 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4224 limits = { 4225 "unary": unary, 4226 "stream": stream, 4227 } 4228 4229 # Raw and parsed data as an output result: 4230 view = { 4231 "rawUserInfo": rawUserInfo, 4232 "rawAccounts": rawAccounts, 4233 "rawMargins": rawMargins, 4234 "rawTariffLimits": rawTariffLimits, 4235 "stat": { 4236 "userInfo": userInfo, 4237 "accounts": accounts, 4238 "margins": margins, 4239 "limits": limits, 4240 }, 4241 } 4242 4243 # --- Prepare text table with user information in human-readable format: 4244 if show: 4245 info = [ 4246 "# Full user information\n\n", 4247 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4248 "## Common information\n\n", 4249 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4250 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4251 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4252 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4253 "\n## User accounts\n\n", 4254 ] 4255 4256 for account in view["stat"]["accounts"].keys(): 4257 info.extend([ 4258 "### ID: [{}]\n\n".format(account), 4259 "| Parameters | Values |\n", 4260 "|----------------------|--------------------------------------------------------------|\n", 4261 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4262 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4263 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4264 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4265 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4266 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4267 ]) 4268 4269 if margins[account]: 4270 info.extend([ 4271 "| Margin status: | Enabled |\n", 4272 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4273 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4274 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4275 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4276 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4277 ]) 4278 4279 else: 4280 info.append("| Margin status: | Disabled |\n\n") 4281 4282 info.extend([ 4283 "\n## Current user tariff limits\n", 4284 "\nSee also:\n", 4285 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4286 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4287 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4288 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4289 "\n### Unary limits\n", 4290 ]) 4291 4292 if unary: 4293 for key, values in sorted(unary.items()): 4294 info.append("\n* Max requests per minute: {}\n".format(key)) 4295 4296 for value in values: 4297 info.append(" - {}\n".format(value)) 4298 4299 else: 4300 info.append("\nNot available\n") 4301 4302 info.append("\n### Stream limits\n") 4303 4304 if stream: 4305 for key, values in sorted(stream.items()): 4306 info.append("\n* Max stream connections: {}\n".format(key)) 4307 4308 for value in values: 4309 info.append(" - {}\n".format(value)) 4310 4311 else: 4312 info.append("\nNot available\n") 4313 4314 infoText = "".join(info) 4315 4316 uLogger.info(infoText) 4317 4318 if self.userInfoFile: 4319 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4320 fH.write(infoText) 4321 4322 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4323 4324 return view
This class implements methods to work with Tinkoff broker server.
Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
About token: https://tinkoff.github.io/investAPI/token/
85 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 86 """ 87 Main class init. 88 89 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 90 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 91 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 92 :param useCache: use default cache file with raw data to use instead of `iList`. 93 True by default. Cache is auto-update if new day has come. 94 If you don't want to use cache and always updates raw data then set `useCache=False`. 95 :param defaultCache: path to default cache file. `dump.json` by default. 96 """ 97 if token is None or not token: 98 try: 99 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 100 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 101 102 except KeyError: 103 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 104 raise Exception("Token required") 105 106 else: 107 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 108 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 109 110 if accountId is None or not accountId: 111 try: 112 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 113 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 114 115 except KeyError: 116 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 117 118 else: 119 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 120 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 121 122 self.version = __version__ # duplicate here used TKSBrokerAPI main version 123 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 124 125 Latest version: https://pypi.org/project/tksbrokerapi/ 126 """ 127 128 self.aliases = TKS_TICKER_ALIASES 129 """Some aliases instead official tickers. 130 131 See also: `TKSEnums.TKS_TICKER_ALIASES` 132 """ 133 134 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 135 136 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 137 138 self.ticker = "" 139 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 140 141 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 142 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 143 144 See also: `SearchByTicker()`, `SearchInstruments()`. 145 """ 146 147 self.figi = "" 148 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 149 150 See also: `SearchByFIGI()`, `SearchInstruments()`. 151 """ 152 153 self.depth = 1 154 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 155 156 See also: `GetCurrentPrices()`. 157 """ 158 159 self.server = r"https://invest-public-api.tinkoff.ru/rest" 160 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 161 162 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 163 """ 164 165 uLogger.debug("Broker API server: {}".format(self.server)) 166 167 self.timeout = 15 168 """Server operations timeout in seconds. Default: `15`. 169 170 See also: `SendAPIRequest()`. 171 """ 172 173 self.headers = { 174 "Content-Type": "application/json", 175 "accept": "application/json", 176 "Authorization": "Bearer {}".format(self.token), 177 "x-app-name": "Tim55667757.TKSBrokerAPI", 178 } 179 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 180 181 See also: `SendAPIRequest()`. 182 """ 183 184 self.body = None 185 """Request body which send to broker server. Default: `None`. 186 187 See also: `SendAPIRequest()`. 188 """ 189 190 self.moreDebug = False 191 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 192 193 self.historyFile = None 194 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 195 196 See also: `History()`. 197 """ 198 199 self.htmlHistoryFile = "index.html" 200 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 201 202 See also: `ShowHistoryChart()`. 203 """ 204 205 self.instrumentsFile = "instruments.md" 206 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 207 208 See also: `ShowInstrumentsInfo()`. 209 """ 210 211 self.searchResultsFile = "search-results.md" 212 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 213 214 See also: `SearchInstruments()`. 215 """ 216 217 self.pricesFile = "prices.md" 218 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 219 220 See also: `GetListOfPrices()`. 221 """ 222 223 self.infoFile = "info.md" 224 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 225 226 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 227 """ 228 229 self.bondsXLSXFile = "ext-bonds.xlsx" 230 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 231 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 232 233 See also: `ExtendBondsData()`. 234 """ 235 236 self.calendarFile = "calendar.md" 237 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 238 239 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 240 241 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 242 """ 243 244 self.overviewFile = "overview.md" 245 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 246 247 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 248 """ 249 250 self.overviewDigestFile = "overview-digest.md" 251 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 252 253 See also: `Overview()` with parameter `details="digest"`. 254 """ 255 256 self.overviewPositionsFile = "overview-positions.md" 257 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 258 259 See also: `Overview()` with parameter `details="positions"`. 260 """ 261 262 self.overviewOrdersFile = "overview-orders.md" 263 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 264 265 See also: `Overview()` with parameter `details="orders"`. 266 """ 267 268 self.overviewAnalyticsFile = "overview-analytics.md" 269 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 270 271 See also: `Overview()` with parameter `details="analytics"`. 272 """ 273 274 self.overviewBondsCalendarFile = "overview-calendar.md" 275 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 276 277 See also: `Overview()` with parameter `details="calendar"`. 278 """ 279 280 self.reportFile = "deals.md" 281 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 282 283 See also: `Deals()`. 284 """ 285 286 self.withdrawalLimitsFile = "limits.md" 287 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 288 289 See also: `OverviewLimits()` and `RequestLimits()`. 290 """ 291 292 self.userInfoFile = "user-info.md" 293 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 294 295 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 296 """ 297 298 self.userAccountsFile = "accounts.md" 299 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 300 301 See also: `OverviewAccounts()`, `RequestAccounts()`. 302 """ 303 304 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 305 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 306 307 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 308 309 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 310 """ 311 312 self.iList = None # init iList for raw instruments data 313 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 314 315 See also: `Listing()`, `DumpInstruments()`. 316 """ 317 318 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 319 if useCache: 320 if os.path.exists(self.iListDumpFile): 321 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 322 curTime = datetime.now(tzutc()) 323 324 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 325 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 326 327 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 328 329 else: 330 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 331 332 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 333 os.path.abspath(self.iListDumpFile), 334 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 335 )) 336 337 else: 338 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 339 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 340 341 else: 342 self.iList = self.Listing() # request new raw instruments data from broker server 343 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 344 345 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 346 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 347 348 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 349 """
Main class init.
Parameters
- token: Bearer token for Tinkoff Invest API. It can be set from environment variable
TKS_API_TOKEN. - accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
Also, this variable can be set from environment variable
TKS_ACCOUNT_ID. - useCache: use default cache file with raw data to use instead of
iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then setuseCache=False. - defaultCache: path to default cache file.
dump.jsonby default.
Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
Latest version: https://pypi.org/project/tksbrokerapi/
String with ticker, e.g. GOOGL. Tickers may be upper case only.
Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc.
More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.
See also: SearchByTicker(), SearchInstruments().
String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.
See also: SearchByFIGI(), SearchInstruments().
Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.
See also: GetCurrentPrices().
Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().
Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.
See also: SendAPIRequest().
Enables more debug information in this class, such as net request and response headers in all methods. False by default.
Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.
See also: History().
Full path to the html file where rendered candles chart stored. Default: index.html.
See also: ShowHistoryChart().
Filename where full available to user instruments list will be saved. Default: instruments.md.
See also: ShowInstrumentsInfo().
Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.
See also: SearchInstruments().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: GetListOfPrices().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().
Filename where wider Pandas DataFrame with more information about bonds: main info, current prices,
bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.
See also: ExtendBondsData().
Filename where bonds payment calendar will be saved. Default: calendar.md.
Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.
See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().
Filename where current portfolio, open trades and orders will be saved. Default: overview.md.
See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().
Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.
See also: Overview() with parameter details="digest".
Filename where only open positions, without everything else will be saved. Default: overview-positions.md.
See also: Overview() with parameter details="positions".
Filename where open limits and stop orders will be saved. Default: overview-orders.md.
See also: Overview() with parameter details="orders".
Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.
See also: Overview() with parameter details="analytics".
Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.
See also: Overview() with parameter details="calendar".
Filename where history of deals and trade statistics will be saved. Default: deals.md.
See also: Deals().
Filename where table of funds available for withdrawal will be saved. Default: limits.md.
See also: OverviewLimits() and RequestLimits().
Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.
See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().
Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.
See also: OverviewAccounts(), RequestAccounts().
Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.
Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.
See also: DumpInstruments() and DumpInstrumentsAsXLSX().
Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.
See also: Listing(), DumpInstruments().
PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
365 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 366 """ 367 Send GET or POST request to broker server and receive JSON object. 368 369 self.header: must be defining with dictionary of headers. 370 self.body: if define then used as request body. None by default. 371 self.timeout: global request timeout, 15 seconds by default. 372 :param url: url with REST request. 373 :param reqType: send "GET" or "POST" request. "GET" by default. 374 :param retry: how many times retry after first request if an 5xx server errors occurred. 375 :param pause: sleep time in seconds between retries. 376 :return: response JSON (dictionary) from broker. 377 """ 378 if reqType not in ("GET", "POST"): 379 uLogger.error("You can define request type: 'GET' or 'POST'!") 380 raise Exception("Incorrect value") 381 382 if self.moreDebug: 383 uLogger.debug("Request parameters:") 384 uLogger.debug(" - REST API URL: {}".format(url)) 385 uLogger.debug(" - request type: {}".format(reqType)) 386 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 387 uLogger.debug(" - body:\n{}".format(self.body)) 388 389 # fast hack to avoid all operations with some tickers/FIGI 390 responseJSON = {} 391 oK = True 392 for item in self.exclude: 393 if item in url: 394 if self.moreDebug: 395 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 396 397 oK = False 398 break 399 400 if oK: 401 counter = 0 402 response = None 403 errMsg = "" 404 405 while not response and counter <= retry: 406 if reqType == "GET": 407 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 408 409 if reqType == "POST": 410 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 411 412 if self.moreDebug: 413 uLogger.debug("Response:") 414 uLogger.debug(" - status code: {}".format(response.status_code)) 415 uLogger.debug(" - reason: {}".format(response.reason)) 416 uLogger.debug(" - body length: {}".format(len(response.text))) 417 uLogger.debug(" - headers:\n{}".format(response.headers)) 418 419 # Server returns some headers: 420 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 421 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 422 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 423 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 424 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 425 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 426 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 427 sleep(rateLimitWait) 428 429 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 430 if 400 <= response.status_code < 500: 431 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 432 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 433 counter = retry + 1 434 435 if 500 <= response.status_code < 600: 436 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 437 uLogger.debug(" - not oK, {}".format(errMsg)) 438 counter += 1 439 440 if counter <= retry: 441 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 442 sleep(pause) 443 444 responseJSON = self._ParseJSON(rawData=response.text) 445 446 if errMsg: 447 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 448 uLogger.error(" - not oK, {}".format(errMsg)) 449 450 return responseJSON
Send GET or POST request to broker server and receive JSON object.
self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.
Parameters
- url: url with REST request.
- reqType: send "GET" or "POST" request. "GET" by default.
- retry: how many times retry after first request if an 5xx server errors occurred.
- pause: sleep time in seconds between retries.
Returns
response JSON (dictionary) from broker.
483 def Listing(self) -> dict: 484 """ 485 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 486 487 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 488 """ 489 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 490 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 491 492 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 493 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 494 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 495 496 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 497 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 498 poolUpdater.close() 499 500 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 501 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 502 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 503 504 # calculate minimum price increment (step) for all instruments and set up instrument's type: 505 for iType in iList.keys(): 506 for ticker in iList[iType]: 507 iList[iType][ticker]["type"] = iType 508 509 if "minPriceIncrement" in iList[iType][ticker].keys(): 510 iList[iType][ticker]["step"] = NanoToFloat( 511 iList[iType][ticker]["minPriceIncrement"]["units"], 512 iList[iType][ticker]["minPriceIncrement"]["nano"], 513 ) 514 515 else: 516 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 517 518 return iList
Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
Returns
Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
520 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 521 """ 522 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 523 524 See also: `DumpInstruments()`, `Listing()`. 525 526 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 527 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 528 """ 529 if self.iListDumpFile is None or not self.iListDumpFile: 530 uLogger.error("Output name of dump file must be defined!") 531 raise Exception("Filename required") 532 533 if not self.iList or forceUpdate: 534 self.iList = self.Listing() 535 536 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 537 538 # Save as XLSX with separated sheets for every type of instruments: 539 with pd.ExcelWriter( 540 path=xlsxDumpFile, 541 date_format=TKS_DATE_FORMAT, 542 datetime_format=TKS_DATE_TIME_FORMAT, 543 mode="w", 544 ) as writer: 545 for iType in TKS_INSTRUMENTS: 546 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 547 df = df[sorted(df)] # sorted by column names 548 df = df.applymap( 549 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 550 na_action="ignore", 551 ) # converting numbers from nano-type to float in every cell 552 df.to_excel( 553 writer, 554 sheet_name=iType, 555 encoding="UTF-8", 556 freeze_panes=(1, 1), 557 ) # saving as XLSX-file with freeze first row and column as headers 558 559 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
See also: DumpInstruments(), Listing().
Parameters
561 def DumpInstruments(self, forceUpdate: bool = True) -> str: 562 """ 563 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 564 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 565 566 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 567 568 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 569 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 570 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 571 """ 572 if self.iListDumpFile is None or not self.iListDumpFile: 573 uLogger.error("Output name of dump file must be defined!") 574 raise Exception("Filename required") 575 576 if not self.iList or forceUpdate: 577 self.iList = self.Listing() 578 579 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 580 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 581 fH.write(jsonDump) 582 583 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 584 585 return jsonDump
Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
using Listing() method. If iListDumpFile string is not empty then also save information to this file.
See also: DumpInstrumentsAsXLSX(), Listing().
Parameters
- forceUpdate: if
Truethen at first updates data withListing()method, otherwise just saves existiListas JSON-file (default:dump.json).
Returns
serialized JSON formatted
strwith full data of instruments, also saved to the--outputJSON-file.
587 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 588 """ 589 Show information about one instrument defined by json data and prints it in Markdown format. 590 591 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 592 593 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 594 :param show: if `True` then also printing information about instrument and its current price. 595 :return: multilines text in Markdown format with information about one instrument. 596 """ 597 splitLine = "| | |\n" 598 infoText = "" 599 600 if iJSON is not None and iJSON and isinstance(iJSON, dict): 601 info = [ 602 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 603 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 604 "| Parameters | Values |\n", 605 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 606 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 607 "| Full name: | {:<54} |\n".format(iJSON["name"]), 608 ] 609 610 if "sector" in iJSON.keys() and iJSON["sector"]: 611 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 612 613 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 614 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 615 616 info.extend([ 617 splitLine, 618 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 619 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 620 ]) 621 622 if "isin" in iJSON.keys() and iJSON["isin"]: 623 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 624 625 if "classCode" in iJSON.keys(): 626 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 627 628 info.extend([ 629 splitLine, 630 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 631 splitLine, 632 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 633 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 634 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 635 ]) 636 637 if iJSON["figi"]: 638 self.figi = iJSON["figi"] 639 iJSON = iJSON | self.RequestTradingStatus() 640 641 info.extend([ 642 splitLine, 643 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 644 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 645 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 646 ]) 647 648 info.append(splitLine) 649 650 if "type" in iJSON.keys() and iJSON["type"]: 651 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 652 653 if "shareType" in iJSON.keys() and iJSON["shareType"]: 654 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 655 656 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 657 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 658 659 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 660 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 661 662 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 663 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 664 665 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 666 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 667 668 if "focusType" in iJSON.keys() and iJSON["focusType"]: 669 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 670 671 if "assetType" in iJSON.keys() and iJSON["assetType"]: 672 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 673 674 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 675 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 676 677 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 678 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 679 680 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 681 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 682 683 if "currency" in iJSON.keys(): 684 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 685 686 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 687 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 688 689 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 690 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 691 692 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 693 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 694 695 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 696 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 697 698 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 699 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 700 701 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 702 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 703 704 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 705 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 706 707 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 708 info.append("| Perpetual bond: | Yes |\n") 709 710 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 711 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 712 713 iExt = None 714 if iJSON["type"] == "Bonds": 715 info.extend([ 716 splitLine, 717 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 718 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 719 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 720 iJSON["nominal"]["currency"], 721 )), 722 ]) 723 724 if "floatingCouponFlag" in iJSON.keys(): 725 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 726 727 if "amortizationFlag" in iJSON.keys(): 728 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 729 730 info.append(splitLine) 731 732 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 733 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 734 735 if iJSON["figi"]: 736 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 737 738 info.extend([ 739 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 740 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 741 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 742 ]) 743 744 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 745 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 746 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 747 iJSON["aciValue"]["currency"] 748 ))) 749 750 if "currentPrice" in iJSON.keys(): 751 info.append(splitLine) 752 753 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 754 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 755 756 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 757 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 758 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 759 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 760 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 761 762 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 763 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 764 765 info.extend([ 766 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 767 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 768 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 769 )), 770 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 771 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 772 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 773 )), 774 "| Changes between last deal price and last close | {:<54} |\n".format( 775 "{:.2f}%{}".format( 776 iJSON["currentPrice"]["changes"], 777 " ({}{:.2f} {})".format( 778 "+" if bondChangesDelta > 0 else "", 779 bondChangesDelta, 780 aciCurrency 781 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 782 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 783 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 784 currency 785 ), 786 ) 787 ), 788 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 789 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 790 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 791 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 792 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 793 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 794 )), 795 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 796 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 797 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 798 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 799 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 800 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 801 )), 802 ]) 803 804 if "lot" in iJSON.keys(): 805 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 806 807 if "step" in iJSON.keys() and iJSON["step"] != 0: 808 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 809 810 # Add bond payment calendar: 811 if iJSON["type"] == "Bonds": 812 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 813 info.extend(["\n", strCalendar]) 814 815 infoText += "".join(info) 816 817 if show: 818 uLogger.info("{}".format(infoText)) 819 820 else: 821 uLogger.debug("{}".format(infoText)) 822 823 if self.infoFile is not None: 824 with open(self.infoFile, "w", encoding="UTF-8") as fH: 825 fH.write(infoText) 826 827 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 828 829 return infoText
Show information about one instrument defined by json data and prints it in Markdown format.
See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().
Parameters
- iJSON: json data of instrument, example:
iJSON = self.iList["Shares"][self.ticker] - show: if
Truethen also printing information about instrument and its current price.
Returns
multilines text in Markdown format with information about one instrument.
831 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 832 """ 833 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 834 835 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 836 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 837 :return: JSON formatted data with information about instrument. 838 """ 839 tickerJSON = {} 840 if self.moreDebug: 841 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 842 843 if not self.ticker: 844 uLogger.warning("self.ticker variable is not be empty!") 845 846 else: 847 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 848 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 849 raise Exception("Instrument not allowed") 850 851 if not self.iList: 852 self.iList = self.Listing() 853 854 if self.ticker in self.iList["Shares"].keys(): 855 tickerJSON = self.iList["Shares"][self.ticker] 856 if self.moreDebug: 857 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 858 859 elif self.ticker in self.iList["Currencies"].keys(): 860 tickerJSON = self.iList["Currencies"][self.ticker] 861 if self.moreDebug: 862 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 863 864 elif self.ticker in self.iList["Bonds"].keys(): 865 tickerJSON = self.iList["Bonds"][self.ticker] 866 if self.moreDebug: 867 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 868 869 elif self.ticker in self.iList["Etfs"].keys(): 870 tickerJSON = self.iList["Etfs"][self.ticker] 871 if self.moreDebug: 872 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 873 874 elif self.ticker in self.iList["Futures"].keys(): 875 tickerJSON = self.iList["Futures"][self.ticker] 876 if self.moreDebug: 877 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 878 879 if tickerJSON: 880 self.figi = tickerJSON["figi"] 881 882 if requestPrice: 883 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 884 885 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 886 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 887 888 else: 889 tickerJSON["currentPrice"]["changes"] = 0 890 891 if show: 892 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 893 894 else: 895 if show: 896 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 897 898 return tickerJSON
Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (because this is long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
900 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 901 """ 902 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 903 904 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 905 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 906 :return: JSON formatted data with information about instrument. 907 """ 908 figiJSON = {} 909 if self.moreDebug: 910 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 911 912 if not self.figi: 913 uLogger.warning("self.figi variable is not be empty!") 914 915 else: 916 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 917 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 918 raise Exception("Instrument not allowed") 919 920 if not self.iList: 921 self.iList = self.Listing() 922 923 for item in self.iList["Shares"].keys(): 924 if self.figi == self.iList["Shares"][item]["figi"]: 925 figiJSON = self.iList["Shares"][item] 926 927 if self.moreDebug: 928 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 929 930 break 931 932 if not figiJSON: 933 for item in self.iList["Currencies"].keys(): 934 if self.figi == self.iList["Currencies"][item]["figi"]: 935 figiJSON = self.iList["Currencies"][item] 936 937 if self.moreDebug: 938 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 939 940 break 941 942 if not figiJSON: 943 for item in self.iList["Bonds"].keys(): 944 if self.figi == self.iList["Bonds"][item]["figi"]: 945 figiJSON = self.iList["Bonds"][item] 946 947 if self.moreDebug: 948 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 949 950 break 951 952 if not figiJSON: 953 for item in self.iList["Etfs"].keys(): 954 if self.figi == self.iList["Etfs"][item]["figi"]: 955 figiJSON = self.iList["Etfs"][item] 956 957 if self.moreDebug: 958 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 959 960 break 961 962 if not figiJSON: 963 for item in self.iList["Futures"].keys(): 964 if self.figi == self.iList["Futures"][item]["figi"]: 965 figiJSON = self.iList["Futures"][item] 966 967 if self.moreDebug: 968 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 969 970 break 971 972 if figiJSON: 973 self.figi = figiJSON["figi"] 974 self.ticker = figiJSON["ticker"] 975 976 if requestPrice: 977 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 978 979 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 980 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 981 982 else: 983 figiJSON["currentPrice"]["changes"] = 0 984 985 if show: 986 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 987 988 else: 989 if show: 990 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 991 992 return figiJSON
Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (it's long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
994 def GetCurrentPrices(self, show: bool = True) -> dict: 995 """ 996 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 997 `{"buy": [{"price": 1243.8, "quantity": 193}, 998 {"price": 1244.0, "quantity": 168}, 999 {"price": 1244.8, "quantity": 5}, 1000 {"price": 1245.0, "quantity": 61}, 1001 {"price": 1245.4, "quantity": 60}], 1002 "sell": [{"price": 1243.6, "quantity": 8}, 1003 {"price": 1242.6, "quantity": 10}, 1004 {"price": 1242.4, "quantity": 18}, 1005 {"price": 1242.2, "quantity": 50}, 1006 {"price": 1242.0, "quantity": 113}], 1007 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1008 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1009 - sell: list of dicts with Buyers prices, 1010 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1011 - quantity: volume value by current price in lots, 1012 - limitUp: current trade session limit price, maximum, 1013 - limitDown: current trade session limit price, minimum, 1014 - lastPrice: last deal price of the instrument, 1015 - closePrice: previous trade session close price of the instrument. 1016 1017 See also: `SearchByTicker()` and `SearchByFIGI()`. 1018 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1019 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1020 1021 :param show: if `True` then print DOM to log and console. 1022 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1023 If an error occurred then returns an empty record: 1024 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1025 """ 1026 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1027 1028 if self.depth < 1: 1029 uLogger.error("Depth of Market (DOM) must be >=1!") 1030 raise Exception("Incorrect value") 1031 1032 if not (self.ticker or self.figi): 1033 uLogger.error("self.ticker or self.figi variables must be defined!") 1034 raise Exception("Ticker or FIGI required") 1035 1036 if self.ticker and not self.figi: 1037 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1038 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1039 1040 if not self.ticker and self.figi: 1041 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1042 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1043 1044 if not self.figi: 1045 uLogger.error("FIGI is not defined!") 1046 raise Exception("Ticker or FIGI required") 1047 1048 else: 1049 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1050 1051 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1052 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1053 self.body = str({"figi": self.figi, "depth": self.depth}) 1054 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1055 1056 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1057 # list of dicts with sellers orders: 1058 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1059 1060 # list of dicts with buyers orders: 1061 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1062 1063 # max price of instrument at this time: 1064 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1065 1066 # min price of instrument at this time: 1067 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1068 1069 # last price of deal with instrument: 1070 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1071 1072 # last close price of instrument: 1073 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1074 1075 else: 1076 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1077 uLogger.debug("Server response: {}".format(pricesResponse)) 1078 1079 if show: 1080 if prices["buy"] or prices["sell"]: 1081 info = [ 1082 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1083 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1084 self.ticker, 1085 self.figi, 1086 self.depth, 1087 ), 1088 "-" * 60, "\n", 1089 " Orders of Buyers | Orders of Sellers\n", 1090 "-" * 60, "\n", 1091 " Sell prices (volumes) | Buy prices (volumes)\n", 1092 "-" * 60, "\n", 1093 ] 1094 1095 if not prices["buy"]: 1096 info.append(" | No orders!\n") 1097 sumBuy = 0 1098 1099 else: 1100 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1101 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1102 for item in maxMinSorted: 1103 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1104 1105 if not prices["sell"]: 1106 info.append("No orders! |\n") 1107 sumSell = 0 1108 1109 else: 1110 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1111 for item in prices["sell"]: 1112 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1113 1114 info.extend([ 1115 "-" * 60, "\n", 1116 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1117 "-" * 60, "\n", 1118 ]) 1119 1120 infoText = "".join(info) 1121 1122 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1123 1124 else: 1125 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1126 1127 return prices
Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5:
{"buy": [{"price": 1243.8, "quantity": 193},
{"price": 1244.0, "quantity": 168},
{"price": 1244.8, "quantity": 5},
{"price": 1245.0, "quantity": 61},
{"price": 1245.4, "quantity": 60}],
"sell": [{"price": 1243.6, "quantity": 8},
{"price": 1242.6, "quantity": 10},
{"price": 1242.4, "quantity": 18},
{"price": 1242.2, "quantity": 50},
{"price": 1242.0, "quantity": 113}],
"limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:
- buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
- sell: list of dicts with Buyers prices,
- price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
- quantity: volume value by current price in lots,
- limitUp: current trade session limit price, maximum,
- limitDown: current trade session limit price, minimum,
- lastPrice: last deal price of the instrument,
- closePrice: previous trade session close price of the instrument.
See also: SearchByTicker() and SearchByFIGI().
REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
Parameters
- show: if
Truethen print DOM to log and console.
Returns
orders book dict with lists of current buy and sell prices:
{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record:{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.
1129 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1130 """ 1131 This method get and show information about all available broker instruments for current user account. 1132 If `instrumentsFile` string is not empty then also save information to this file. 1133 1134 :param show: if `True` then print results to console, if `False` — print only to file. 1135 :return: multi-lines string with all available broker instruments 1136 """ 1137 if not self.iList: 1138 self.iList = self.Listing() 1139 1140 info = [ 1141 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1142 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1143 ] 1144 1145 # add instruments count by type: 1146 for iType in self.iList.keys(): 1147 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1148 1149 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1150 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1151 1152 # generating info tables with all instruments by type: 1153 for iType in self.iList.keys(): 1154 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1155 1156 for instrument in self.iList[iType].keys(): 1157 iName = self.iList[iType][instrument]["name"] # instrument's name 1158 if len(iName) > 57: 1159 iName = "{}...".format(iName[:54]) # right trim for a long string 1160 1161 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1162 self.iList[iType][instrument]["ticker"], 1163 iName, 1164 self.iList[iType][instrument]["figi"], 1165 self.iList[iType][instrument]["currency"], 1166 self.iList[iType][instrument]["lot"], 1167 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1168 )) 1169 1170 infoText = "".join(info) 1171 1172 if show: 1173 uLogger.info(infoText) 1174 1175 if self.instrumentsFile: 1176 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1177 fH.write(infoText) 1178 1179 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1180 1181 return infoText
This method get and show information about all available broker instruments for current user account.
If instrumentsFile string is not empty then also save information to this file.
Parameters
- show: if
Truethen print results to console, ifFalse— print only to file.
Returns
multi-lines string with all available broker instruments
1183 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1184 """ 1185 This method search and show information about instruments by part of its ticker, FIGI or name. 1186 If `searchResultsFile` string is not empty then also save information to this file. 1187 1188 :param pattern: string with part of ticker, FIGI or instrument's name. 1189 :param show: if `True` then print results to console, if `False` — return list of result only. 1190 :return: list of dictionaries with all found instruments. 1191 """ 1192 if not self.iList: 1193 self.iList = self.Listing() 1194 1195 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1196 compiledPattern = re.compile(pattern, re.IGNORECASE) 1197 1198 for iType in self.iList: 1199 for instrument in self.iList[iType].values(): 1200 searchResult = compiledPattern.search(" ".join( 1201 [instrument["ticker"], instrument["figi"], instrument["name"]] 1202 )) 1203 1204 if searchResult: 1205 searchResults[iType][instrument["ticker"]] = instrument 1206 1207 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1208 info = [ 1209 "# Search results\n\n", 1210 "* **Search pattern:** [{}]\n".format(pattern), 1211 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1212 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1213 ] 1214 infoShort = info[:] 1215 1216 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1217 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1218 skippedLine = "| ... | ... | ... | ... |\n" 1219 1220 if resultsLen == 0: 1221 info.append("\nNo results\n") 1222 infoShort.append("\nNo results\n") 1223 uLogger.warning("No results. Try changing your search pattern.") 1224 1225 else: 1226 for iType in searchResults: 1227 iTypeValuesCount = len(searchResults[iType].values()) 1228 if iTypeValuesCount > 0: 1229 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1230 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1231 1232 for instrument in searchResults[iType].values(): 1233 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1234 instrument["type"], 1235 instrument["ticker"], 1236 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1237 instrument["figi"], 1238 )) 1239 1240 if iTypeValuesCount <= 5: 1241 infoShort.extend(info[-iTypeValuesCount:]) 1242 1243 else: 1244 infoShort.extend(info[-5:]) 1245 infoShort.append(skippedLine) 1246 1247 infoText = "".join(info) 1248 infoTextShort = "".join(infoShort) 1249 1250 if show: 1251 uLogger.info(infoTextShort) 1252 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1253 1254 if self.searchResultsFile: 1255 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1256 fH.write(infoText) 1257 1258 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1259 1260 return searchResults
This method search and show information about instruments by part of its ticker, FIGI or name.
If searchResultsFile string is not empty then also save information to this file.
Parameters
- pattern: string with part of ticker, FIGI or instrument's name.
- show: if
Truethen print results to console, ifFalse— return list of result only.
Returns
list of dictionaries with all found instruments.
1262 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1263 """ 1264 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1265 1266 :param instruments: list of strings with tickers or FIGIs. 1267 :return: list with unique instrument FIGIs only. 1268 """ 1269 requestedInstruments = [] 1270 for iName in instruments: 1271 if iName not in self.aliases.keys(): 1272 if iName not in requestedInstruments: 1273 requestedInstruments.append(iName) 1274 1275 else: 1276 if iName not in requestedInstruments: 1277 if self.aliases[iName] not in requestedInstruments: 1278 requestedInstruments.append(self.aliases[iName]) 1279 1280 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1281 1282 onlyUniqueFIGIs = [] 1283 for iName in requestedInstruments: 1284 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1285 continue 1286 1287 self.ticker = iName 1288 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1289 1290 if not iData: 1291 self.ticker = "" 1292 self.figi = iName 1293 1294 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1295 1296 if not iData: 1297 self.figi = "" 1298 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1299 1300 if iData and iData["figi"] not in onlyUniqueFIGIs: 1301 onlyUniqueFIGIs.append(iData["figi"]) 1302 1303 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1304 1305 return onlyUniqueFIGIs
Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
Parameters
- instruments: list of strings with tickers or FIGIs.
Returns
list with unique instrument FIGIs only.
1307 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1308 """ 1309 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1310 1311 See limits: https://tinkoff.github.io/investAPI/limits/ 1312 1313 If `pricesFile` string is not empty then also save information to this file. 1314 1315 :param instruments: list of strings with tickers or FIGIs. 1316 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1317 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1318 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1319 """ 1320 if instruments is None or not instruments: 1321 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1322 raise Exception("Ticker or FIGI required") 1323 1324 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1325 1326 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1327 1328 iList = [] # trying to get info and current prices about all unique instruments: 1329 for self.figi in onlyUniqueFIGIs: 1330 iData = self.SearchByFIGI(requestPrice=True) 1331 iList.append(iData) 1332 1333 self.ShowListOfPrices(iList, show) 1334 1335 return iList
This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
See limits: https://tinkoff.github.io/investAPI/limits/
If pricesFile string is not empty then also save information to this file.
Parameters
- instruments: list of strings with tickers or FIGIs.
- show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile.
Returns
list of instruments looks like
[{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker()orSearchByFIGI()methods.
1337 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1338 """ 1339 Show table contains current prices of given instruments. 1340 1341 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1342 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1343 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1344 :return: multilines text in Markdown format as a table contains current prices. 1345 """ 1346 infoText = "" 1347 1348 if show or self.pricesFile: 1349 info = [ 1350 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1351 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1352 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1353 ] 1354 1355 for item in iList: 1356 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1357 item["ticker"], 1358 item["figi"], 1359 item["type"], 1360 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1361 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1362 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1363 "{} / {}".format( 1364 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1365 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1366 ), 1367 "{} / {}".format( 1368 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1369 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1370 ), 1371 item["currency"], 1372 )) 1373 1374 infoText = "".join(info) 1375 1376 if show: 1377 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1378 1379 if self.pricesFile: 1380 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1381 fH.write(infoText) 1382 1383 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1384 1385 return infoText
Show table contains current prices of given instruments.
Parameters
- **iList: list of instruments looks like
[{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker(requestPrice=True)or bySearchByFIGI(requestPrice=True)methods. - show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile.
Returns
multilines text in Markdown format as a table contains current prices.
1387 def RequestTradingStatus(self) -> dict: 1388 """ 1389 Requesting trading status for the instrument defined by `figi` variable. 1390 1391 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1392 1393 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1394 1395 :return: dictionary with trading status attributes. Response example: 1396 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1397 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1398 """ 1399 if self.figi is None or not self.figi: 1400 uLogger.error("Variable `figi` must be defined for using this method!") 1401 raise Exception("FIGI required") 1402 1403 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1404 1405 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1406 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1407 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1408 1409 if self.moreDebug: 1410 uLogger.debug("Records about current trading status successfully received") 1411 1412 return tradingStatus
Requesting trading status for the instrument defined by figi variable.
Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
Returns
dictionary with trading status attributes. Response example:
{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}
1414 def RequestPortfolio(self) -> dict: 1415 """ 1416 Requesting actual user's portfolio for current `accountId`. 1417 1418 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1419 1420 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1421 1422 :return: dictionary with user's portfolio. 1423 """ 1424 if self.accountId is None or not self.accountId: 1425 uLogger.error("Variable `accountId` must be defined for using this method!") 1426 raise Exception("Account ID required") 1427 1428 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1429 1430 self.body = str({"accountId": self.accountId}) 1431 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1432 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1433 1434 if self.moreDebug: 1435 uLogger.debug("Records about user's portfolio successfully received") 1436 1437 return rawPortfolio
Requesting actual user's portfolio for current accountId.
REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
Returns
dictionary with user's portfolio.
1439 def RequestPositions(self) -> dict: 1440 """ 1441 Requesting open positions by currencies and instruments for current `accountId`. 1442 1443 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1444 1445 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1446 1447 :return: dictionary with open positions by instruments. 1448 """ 1449 if self.accountId is None or not self.accountId: 1450 uLogger.error("Variable `accountId` must be defined for using this method!") 1451 raise Exception("Account ID required") 1452 1453 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1454 1455 self.body = str({"accountId": self.accountId}) 1456 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1457 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1458 1459 if self.moreDebug: 1460 uLogger.debug("Records about current open positions successfully received") 1461 1462 return rawPositions
Requesting open positions by currencies and instruments for current accountId.
REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
Returns
dictionary with open positions by instruments.
1464 def RequestPendingOrders(self) -> list: 1465 """ 1466 Requesting current actual pending orders for current `accountId`. 1467 1468 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1469 1470 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1471 1472 :return: list of dictionaries with pending orders. 1473 """ 1474 if self.accountId is None or not self.accountId: 1475 uLogger.error("Variable `accountId` must be defined for using this method!") 1476 raise Exception("Account ID required") 1477 1478 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1479 1480 self.body = str({"accountId": self.accountId}) 1481 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1482 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1483 1484 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1485 1486 return rawOrders
Requesting current actual pending orders for current accountId.
REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
Returns
list of dictionaries with pending orders.
1488 def RequestStopOrders(self) -> list: 1489 """ 1490 Requesting current actual stop orders for current `accountId`. 1491 1492 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1493 1494 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1495 1496 :return: list of dictionaries with stop orders. 1497 """ 1498 if self.accountId is None or not self.accountId: 1499 uLogger.error("Variable `accountId` must be defined for using this method!") 1500 raise Exception("Account ID required") 1501 1502 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1503 1504 self.body = str({"accountId": self.accountId}) 1505 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1506 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1507 1508 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1509 1510 return rawStopOrders
Requesting current actual stop orders for current accountId.
REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
Returns
list of dictionaries with stop orders.
1512 def Overview(self, show: bool = False, details: str = "full") -> dict: 1513 """ 1514 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1515 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1516 and `overviewBondsCalendarFile` are defined then also save information to file. 1517 1518 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1519 many requests about the state of the portfolio, and then, based on the received data, a large number 1520 of calculation and statistics are collected. 1521 1522 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1523 :param details: how detailed should the information be? 1524 - `full` — shows full available information about portfolio status (by default), 1525 - `positions` — shows only open positions, 1526 - `orders` — shows only sections of open limits and stop orders. 1527 - `digest` — show a short digest of the portfolio status, 1528 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1529 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1530 :return: dictionary with client's raw portfolio and some statistics. 1531 """ 1532 if self.accountId is None or not self.accountId: 1533 uLogger.error("Variable `accountId` must be defined for using this method!") 1534 raise Exception("Account ID required") 1535 1536 view = { 1537 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1538 "headers": {}, # list of dictionaries, response headers without "positions" section 1539 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1540 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1541 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1542 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1543 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1544 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1545 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1546 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1547 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1548 }, 1549 "stat": { # --- some statistics calculated using "raw" sections: 1550 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1551 "availableRUB": 0., # available rubles (without other currencies) 1552 "blockedRUB": 0., # blocked sum in Russian Rouble 1553 "totalChangesRUB": 0., # changes for all open trades in RUB 1554 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1555 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1556 "sharesCostRUB": 0., # costs of all shares in RUB 1557 "bondsCostRUB": 0., # costs of all bonds in RUB 1558 "etfsCostRUB": 0., # costs of all etfs in RUB 1559 "futuresCostRUB": 0., # costs of all futures in RUB 1560 "Currencies": [], # list of dictionaries of all currencies statistics 1561 "Shares": [], # list of dictionaries of all shares statistics 1562 "Bonds": [], # list of dictionaries of all bonds statistics 1563 "Etfs": [], # list of dictionaries of all etfs statistics 1564 "Futures": [], # list of dictionaries of all futures statistics 1565 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1566 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1567 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1568 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1569 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1570 }, 1571 "analytics": { # --- some analytics of portfolio: 1572 "distrByAssets": {}, # portfolio distribution by assets 1573 "distrByCompanies": {}, # portfolio distribution by companies 1574 "distrBySectors": {}, # portfolio distribution by sectors 1575 "distrByCurrencies": {}, # portfolio distribution by currencies 1576 "distrByCountries": {}, # portfolio distribution by countries 1577 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1578 } 1579 } 1580 1581 details = details.lower() 1582 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1583 if details not in availableDetails: 1584 details = "full" 1585 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1586 1587 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1588 1589 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1590 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1591 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1592 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1593 1594 # save response headers without "positions" section: 1595 for key in portfolioResponse.keys(): 1596 if key != "positions": 1597 view["raw"]["headers"][key] = portfolioResponse[key] 1598 1599 else: 1600 continue 1601 1602 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1603 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1604 for item in portfolioResponse["positions"]: 1605 if item["instrumentType"] == "currency": 1606 self.figi = item["figi"] 1607 curr = self.SearchByFIGI(requestPrice=False) 1608 1609 # current price of currency in RUB: 1610 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1611 "name": curr["name"], 1612 "currentPrice": NanoToFloat( 1613 item["currentPrice"]["units"], 1614 item["currentPrice"]["nano"] 1615 ), 1616 } 1617 1618 view["raw"]["Currencies"].append(item) 1619 1620 elif item["instrumentType"] == "share": 1621 view["raw"]["Shares"].append(item) 1622 1623 elif item["instrumentType"] == "bond": 1624 view["raw"]["Bonds"].append(item) 1625 1626 elif item["instrumentType"] == "etf": 1627 view["raw"]["Etfs"].append(item) 1628 1629 elif item["instrumentType"] == "futures": 1630 view["raw"]["Futures"].append(item) 1631 1632 else: 1633 continue 1634 1635 # how many volume of currencies (by ISO currency name) are blocked: 1636 for item in view["raw"]["positions"]["blocked"]: 1637 blocked = NanoToFloat(item["units"], item["nano"]) 1638 if blocked > 0: 1639 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1640 1641 # how many volume of instruments (by FIGI) are blocked: 1642 for item in view["raw"]["positions"]["securities"]: 1643 blocked = int(item["blocked"]) 1644 if blocked > 0: 1645 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1646 1647 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1648 1649 if "rub" in allBlocked.keys(): 1650 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1651 1652 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1653 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1654 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1655 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1656 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1657 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1658 view["stat"]["portfolioCostRUB"] = sum([ 1659 view["stat"]["allCurrenciesCostRUB"], 1660 view["stat"]["sharesCostRUB"], 1661 view["stat"]["bondsCostRUB"], 1662 view["stat"]["etfsCostRUB"], 1663 view["stat"]["futuresCostRUB"], 1664 ]) 1665 1666 # --- calculating some portfolio statistics: 1667 byComp = {} # distribution by companies 1668 bySect = {} # distribution by sectors 1669 byCurr = {} # distribution by currencies (include RUB) 1670 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1671 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1672 1673 for item in portfolioResponse["positions"]: 1674 self.figi = item["figi"] 1675 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1676 1677 if instrument: 1678 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1679 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1680 1681 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1682 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1683 1684 else: 1685 blocked = 0 1686 1687 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1688 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1689 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1690 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1691 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1692 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1693 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1694 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1695 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1696 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1697 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1698 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1699 1700 statData = { 1701 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1702 "ticker": instrument["ticker"], # ticker by FIGI 1703 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1704 "volume": volume, # available volume of instrument 1705 "lots": lots, # volume in lots of instrument 1706 "direction": direction, # direction of an instrument's position: short or long 1707 "blocked": blocked, # blocked volume of currency or instrument 1708 "currentPrice": curPrice, # current instrument's price in basic asset 1709 "average": average, # current average position price 1710 "cost": cost, # current cost of all volume of instrument in basic asset 1711 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1712 "costRUB": costRUB, # cost of instrument in ruble 1713 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1714 "profit": profit, # expected profit at current moment 1715 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1716 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1717 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1718 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1719 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1720 "step": instrument["step"], # minimum price increment 1721 } 1722 1723 # adding distribution by unique countries: 1724 if statData["country"] not in byCountry.keys(): 1725 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1726 1727 else: 1728 byCountry[statData["country"]]["cost"] += costRUB 1729 byCountry[statData["country"]]["percent"] += percentCostRUB 1730 1731 if item["instrumentType"] != "currency": 1732 # adding distribution by unique companies: 1733 if statData["name"]: 1734 if statData["name"] not in byComp.keys(): 1735 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1736 1737 else: 1738 byComp[statData["name"]]["cost"] += costRUB 1739 byComp[statData["name"]]["percent"] += percentCostRUB 1740 1741 # adding distribution by unique sectors: 1742 if statData["sector"] not in bySect.keys(): 1743 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1744 1745 else: 1746 bySect[statData["sector"]]["cost"] += costRUB 1747 bySect[statData["sector"]]["percent"] += percentCostRUB 1748 1749 # adding distribution by unique currencies: 1750 if currency not in byCurr.keys(): 1751 byCurr[currency] = { 1752 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1753 "cost": costRUB, 1754 "percent": percentCostRUB 1755 } 1756 1757 else: 1758 byCurr[currency]["cost"] += costRUB 1759 byCurr[currency]["percent"] += percentCostRUB 1760 1761 # saving statistics for every instrument: 1762 if item["instrumentType"] == "currency": 1763 view["stat"]["Currencies"].append(statData) 1764 1765 # update dict with free funds for trading (total - blocked) by currencies 1766 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1767 view["stat"]["funds"][currency] = { 1768 "total": volume, 1769 "totalCostRUB": costRUB, # total volume cost in rubles 1770 "free": volume - blocked, 1771 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1772 } 1773 1774 elif item["instrumentType"] == "share": 1775 view["stat"]["Shares"].append(statData) 1776 1777 elif item["instrumentType"] == "bond": 1778 view["stat"]["Bonds"].append(statData) 1779 1780 elif item["instrumentType"] == "etf": 1781 view["stat"]["Etfs"].append(statData) 1782 1783 elif item["instrumentType"] == "Futures": 1784 view["stat"]["Futures"].append(statData) 1785 1786 else: 1787 continue 1788 1789 # total changes in Russian Ruble: 1790 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1791 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1792 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1793 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1794 view["stat"]["funds"]["rub"] = { 1795 "total": view["stat"]["availableRUB"], 1796 "totalCostRUB": view["stat"]["availableRUB"], 1797 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1798 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1799 } 1800 1801 # --- pending orders sector data: 1802 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending orders to avoid many times price requests 1803 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1804 1805 for item in view["raw"]["orders"]: 1806 self.figi = item["figi"] 1807 1808 if item["figi"] not in uniquePendingOrdersFIGIs: 1809 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1810 1811 uniquePendingOrdersFIGIs.append(item["figi"]) 1812 uniquePendingOrders[item["figi"]] = instrument 1813 1814 else: 1815 instrument = uniquePendingOrders[item["figi"]] 1816 1817 if instrument: 1818 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1819 orderType = TKS_ORDER_TYPES[item["orderType"]] 1820 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1821 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1822 1823 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1824 if item["direction"] == "ORDER_DIRECTION_BUY": 1825 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1826 1827 else: 1828 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1829 1830 # requested price for order execution: 1831 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1832 1833 # necessary changes in percent to reach target from current price: 1834 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1835 1836 view["stat"]["orders"].append({ 1837 "orderID": item["orderId"], # orderId number parameter of current order 1838 "figi": item["figi"], # FIGI identification 1839 "ticker": instrument["ticker"], # ticker name by FIGI 1840 "lotsRequested": item["lotsRequested"], # requested lots value 1841 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1842 "currentPrice": lastPrice, # current instrument's price for defined action 1843 "targetPrice": target, # requested price for order execution in base currency 1844 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1845 "percentChanges": changes, # changes in percent to target from current price 1846 "currency": item["currency"], # instrument's currency name 1847 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1848 "type": orderType, # type of order from TKS_ORDER_TYPES 1849 "status": orderState, # order status from TKS_ORDER_STATES 1850 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1851 }) 1852 1853 # --- stop orders sector data: 1854 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1855 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1856 1857 for item in view["raw"]["stopOrders"]: 1858 self.figi = item["figi"] 1859 1860 if item["figi"] not in uniqueStopOrdersFIGIs: 1861 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1862 1863 uniqueStopOrdersFIGIs.append(item["figi"]) 1864 uniqueStopOrders[item["figi"]] = instrument 1865 1866 else: 1867 instrument = uniqueStopOrders[item["figi"]] 1868 1869 if instrument: 1870 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1871 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1872 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1873 1874 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1875 if "expirationTime" in item.keys(): 1876 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1877 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1878 1879 else: 1880 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1881 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1882 1883 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1884 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1885 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1886 1887 else: 1888 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1889 1890 # requested price when stop-order executed: 1891 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1892 1893 # price for limit-order, set up when stop-order executed: 1894 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1895 1896 # necessary changes in percent to reach target from current price: 1897 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1898 1899 view["stat"]["stopOrders"].append({ 1900 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1901 "figi": item["figi"], # FIGI identification 1902 "ticker": instrument["ticker"], # ticker name by FIGI 1903 "lotsRequested": item["lotsRequested"], # requested lots value 1904 "currentPrice": lastPrice, # current instrument's price for defined action 1905 "targetPrice": target, # requested price for stop-order execution in base currency 1906 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1907 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1908 "percentChanges": changes, # changes in percent to target from current price 1909 "currency": item["currency"], # instrument's currency name 1910 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1911 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1912 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1913 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1914 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1915 }) 1916 1917 # --- calculating data for analytics section: 1918 # portfolio distribution by assets: 1919 view["analytics"]["distrByAssets"] = { 1920 "Ruble": { 1921 "uniques": 1, 1922 "cost": view["stat"]["availableRUB"], 1923 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1924 }, 1925 "Currencies": { 1926 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 1927 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 1928 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1929 }, 1930 "Shares": { 1931 "uniques": len(view["stat"]["Shares"]), 1932 "cost": view["stat"]["sharesCostRUB"], 1933 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1934 }, 1935 "Bonds": { 1936 "uniques": len(view["stat"]["Bonds"]), 1937 "cost": view["stat"]["bondsCostRUB"], 1938 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1939 }, 1940 "Etfs": { 1941 "uniques": len(view["stat"]["Etfs"]), 1942 "cost": view["stat"]["etfsCostRUB"], 1943 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1944 }, 1945 "Futures": { 1946 "uniques": len(view["stat"]["Futures"]), 1947 "cost": view["stat"]["futuresCostRUB"], 1948 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1949 }, 1950 } 1951 1952 # portfolio distribution by companies: 1953 view["analytics"]["distrByCompanies"]["All money cash"] = { 1954 "ticker": "", 1955 "cost": view["stat"]["allCurrenciesCostRUB"], 1956 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1957 } 1958 view["analytics"]["distrByCompanies"].update(byComp) 1959 1960 # portfolio distribution by sectors: 1961 view["analytics"]["distrBySectors"]["All money cash"] = { 1962 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 1963 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 1964 } 1965 view["analytics"]["distrBySectors"].update(bySect) 1966 1967 # portfolio distribution by currencies: 1968 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 1969 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 1970 1971 if self.moreDebug: 1972 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 1973 1974 view["analytics"]["distrByCurrencies"].update(byCurr) 1975 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 1976 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 1977 1978 # portfolio distribution by countries: 1979 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 1980 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 1981 1982 if self.moreDebug: 1983 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 1984 1985 view["analytics"]["distrByCountries"].update(byCountry) 1986 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 1987 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 1988 1989 # --- Prepare text statistics overview in human-readable: 1990 if show: 1991 # Whatever the value `details`, header not changes: 1992 info = [ 1993 "# Client's portfolio\n\n", 1994 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1995 "* **Account ID:** [{}]\n".format(self.accountId), 1996 ] 1997 1998 if details in ["full", "positions", "digest"]: 1999 info.extend([ 2000 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2001 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2002 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2003 view["stat"]["totalChangesRUB"], 2004 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2005 view["stat"]["totalChangesPercentRUB"], 2006 ), 2007 ]) 2008 2009 if details in ["full", "positions"]: 2010 info.extend([ 2011 "## Open positions\n\n", 2012 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2013 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2014 "| Ruble | {:>31} | | | | | |\n".format( 2015 "{:.2f} ({:.2f}) rub".format( 2016 view["stat"]["availableRUB"], 2017 view["stat"]["blockedRUB"], 2018 ) 2019 ) 2020 ]) 2021 2022 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2023 return [ 2024 "| | | | | | | |\n", 2025 "| {:<27} | | | | | {:>19} | |\n".format( 2026 noTradeStr if noTradeStr else typeStr, 2027 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2028 ), 2029 ] 2030 2031 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2032 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2033 "{} [{}]".format(data["ticker"], data["figi"]), 2034 "{:.2f} ({:.2f}) {}".format( 2035 data["volume"], 2036 data["blocked"], 2037 data["currency"], 2038 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2039 data["volume"], 2040 data["blocked"], 2041 ), 2042 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2043 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2044 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2045 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2046 "{}{:.2f} {} ({}{:.2f}%)".format( 2047 "+" if data["profit"] > 0 else "", 2048 data["profit"], data["baseCurrencyName"], 2049 "+" if data["percentProfit"] > 0 else "", 2050 data["percentProfit"], 2051 ), 2052 ) 2053 2054 # --- Show currencies section: 2055 if view["stat"]["Currencies"]: 2056 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2057 for item in view["stat"]["Currencies"]: 2058 info.append(_InfoStr(item, showCurrencyName=True)) 2059 2060 else: 2061 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2062 2063 # --- Show shares section: 2064 if view["stat"]["Shares"]: 2065 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2066 2067 for item in view["stat"]["Shares"]: 2068 info.append(_InfoStr(item)) 2069 2070 else: 2071 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2072 2073 # --- Show bonds section: 2074 if view["stat"]["Bonds"]: 2075 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2076 2077 for item in view["stat"]["Bonds"]: 2078 info.append(_InfoStr(item)) 2079 2080 else: 2081 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2082 2083 # --- Show etfs section: 2084 if view["stat"]["Etfs"]: 2085 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2086 2087 for item in view["stat"]["Etfs"]: 2088 info.append(_InfoStr(item)) 2089 2090 else: 2091 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2092 2093 # --- Show futures section: 2094 if view["stat"]["Futures"]: 2095 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2096 2097 for item in view["stat"]["Futures"]: 2098 info.append(_InfoStr(item)) 2099 2100 else: 2101 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2102 2103 if details in ["full", "orders"]: 2104 # --- Show pending orders section: 2105 if view["stat"]["orders"]: 2106 info.extend([ 2107 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2108 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2109 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2110 ]) 2111 2112 for item in view["stat"]["orders"]: 2113 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2114 "{} [{}]".format(item["ticker"], item["figi"]), 2115 item["orderID"], 2116 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2117 "{} {} ({}{:.2f}%)".format( 2118 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2119 item["baseCurrencyName"], 2120 "+" if item["percentChanges"] > 0 else "", 2121 float(item["percentChanges"]), 2122 ), 2123 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2124 item["action"], 2125 item["type"], 2126 item["date"], 2127 )) 2128 2129 else: 2130 info.append("\n## Total pending limit-orders: 0\n") 2131 2132 # --- Show stop orders section: 2133 if view["stat"]["stopOrders"]: 2134 info.extend([ 2135 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2136 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2137 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2138 ]) 2139 2140 for item in view["stat"]["stopOrders"]: 2141 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2142 "{} [{}]".format(item["ticker"], item["figi"]), 2143 item["orderID"], 2144 item["lotsRequested"], 2145 "{} {} ({}{:.2f}%)".format( 2146 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2147 item["baseCurrencyName"], 2148 "+" if item["percentChanges"] > 0 else "", 2149 float(item["percentChanges"]), 2150 ), 2151 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2152 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2153 item["action"], 2154 item["type"], 2155 item["expType"], 2156 item["createDate"], 2157 item["expDate"], 2158 )) 2159 2160 else: 2161 info.append("\n## Total stop-orders: 0\n") 2162 2163 if details in ["full", "analytics"]: 2164 # -- Show analytics section: 2165 if view["stat"]["portfolioCostRUB"] > 0: 2166 info.extend([ 2167 "\n# Analytics\n" 2168 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2169 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2170 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2171 view["stat"]["totalChangesRUB"], 2172 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2173 view["stat"]["totalChangesPercentRUB"], 2174 ), 2175 "\n## Portfolio distribution by assets\n" 2176 "\n| Type | Uniques | Percent | Current cost |\n", 2177 "|------------------------------------|---------|---------|--------------------|\n", 2178 ]) 2179 2180 for key in view["analytics"]["distrByAssets"].keys(): 2181 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2182 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2183 key, 2184 view["analytics"]["distrByAssets"][key]["uniques"], 2185 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2186 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2187 )) 2188 2189 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2190 2191 info.extend([ 2192 "\n## Portfolio distribution by companies\n" 2193 "\n| Company | Percent | Current cost |\n", 2194 aSepLine, 2195 ]) 2196 2197 for company in view["analytics"]["distrByCompanies"].keys(): 2198 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2199 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2200 "{}{}".format( 2201 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2202 company, 2203 ), 2204 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2205 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2206 )) 2207 2208 info.extend([ 2209 "\n## Portfolio distribution by sectors\n" 2210 "\n| Sector | Percent | Current cost |\n", 2211 aSepLine, 2212 ]) 2213 2214 for sector in view["analytics"]["distrBySectors"].keys(): 2215 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2216 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2217 sector, 2218 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2219 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2220 )) 2221 2222 info.extend([ 2223 "\n## Portfolio distribution by currencies\n" 2224 "\n| Instruments currencies | Percent | Current cost |\n", 2225 aSepLine, 2226 ]) 2227 2228 for curr in view["analytics"]["distrByCurrencies"].keys(): 2229 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2230 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2231 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2232 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2233 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2234 )) 2235 2236 info.extend([ 2237 "\n## Portfolio distribution by countries\n" 2238 "\n| Assets by country | Percent | Current cost |\n", 2239 aSepLine, 2240 ]) 2241 2242 for country in view["analytics"]["distrByCountries"].keys(): 2243 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2244 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2245 country, 2246 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2247 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2248 )) 2249 2250 if details in ["full", "calendar"]: 2251 # -- Show bonds payment calendar section: 2252 if view["stat"]["Bonds"]: 2253 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2254 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2255 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2256 2257 else: 2258 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2259 2260 infoText = "".join(info) 2261 2262 uLogger.info(infoText) 2263 2264 if details == "full" and self.overviewFile: 2265 filename = self.overviewFile 2266 2267 elif details == "digest" and self.overviewDigestFile: 2268 filename = self.overviewDigestFile 2269 2270 elif details == "positions" and self.overviewPositionsFile: 2271 filename = self.overviewPositionsFile 2272 2273 elif details == "orders" and self.overviewOrdersFile: 2274 filename = self.overviewOrdersFile 2275 2276 elif details == "analytics" and self.overviewAnalyticsFile: 2277 filename = self.overviewAnalyticsFile 2278 2279 elif details == "calendar" and self.overviewBondsCalendarFile: 2280 filename = self.overviewBondsCalendarFile 2281 2282 else: 2283 filename = "" 2284 2285 if filename: 2286 with open(filename, "w", encoding="UTF-8") as fH: 2287 fH.write(infoText) 2288 2289 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2290 2291 return view
Get portfolio: all open positions, orders and some statistics for current accountId.
If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile
and overviewBondsCalendarFile are defined then also save information to file.
WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen show more debug information. - details: how detailed should the information be?
full— shows full available information about portfolio status (by default),positions— shows only open positions,orders— shows only sections of open limits and stop orders.digest— show a short digest of the portfolio status,analytics— shows only the analytics section and the distribution of the portfolio by various categories,calendar— shows only the bonds calendar section (if these present in portfolio),
Returns
dictionary with client's raw portfolio and some statistics.
2293 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2294 """ 2295 Returns history operations between two given dates for current `accountId`. 2296 If `reportFile` string is not empty then also save human-readable report. 2297 Shows some statistical data of closed positions. 2298 2299 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2300 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2301 :param show: if `True` then also prints all records to the console. 2302 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2303 :return: original list of dictionaries with history of deals records from API ("operations" key): 2304 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2305 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2306 """ 2307 if self.accountId is None or not self.accountId: 2308 uLogger.error("Variable `accountId` must be defined for using this method!") 2309 raise Exception("Account ID required") 2310 2311 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2312 2313 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2314 2315 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2316 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2317 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2318 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2319 customStat = {} # custom statistics in additional to responseJSON 2320 2321 # --- output report in human-readable format: 2322 if show or self.reportFile: 2323 splitLine1 = "| | | | | |\n" # Summary section 2324 splitLine2 = "| | | | | | | | |\n" # Operations section 2325 nextDay = "" 2326 2327 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2328 2329 if len(ops) > 0: 2330 customStat = { 2331 "opsCount": 0, # total operations count 2332 "buyCount": 0, # buy operations 2333 "sellCount": 0, # sell operations 2334 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2335 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2336 "payIn": {"rub": 0.}, # Deposit brokerage account 2337 "payOut": {"rub": 0.}, # Withdrawals 2338 "divs": {"rub": 0.}, # Dividends income 2339 "coupons": {"rub": 0.}, # Coupon's income 2340 "brokerCom": {"rub": 0.}, # Service commissions 2341 "serviceCom": {"rub": 0.}, # Service commissions 2342 "marginCom": {"rub": 0.}, # Margin commissions 2343 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2344 } 2345 2346 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2347 for item in ops: 2348 if item["state"] == "OPERATION_STATE_EXECUTED": 2349 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2350 2351 # count buy operations: 2352 if "_BUY" in item["operationType"]: 2353 customStat["buyCount"] += 1 2354 2355 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2356 customStat["buyTotal"][item["payment"]["currency"]] += payment 2357 2358 else: 2359 customStat["buyTotal"][item["payment"]["currency"]] = payment 2360 2361 # count sell operations: 2362 elif "_SELL" in item["operationType"]: 2363 customStat["sellCount"] += 1 2364 2365 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2366 customStat["sellTotal"][item["payment"]["currency"]] += payment 2367 2368 else: 2369 customStat["sellTotal"][item["payment"]["currency"]] = payment 2370 2371 # count incoming operations: 2372 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2373 if item["payment"]["currency"] in customStat["payIn"].keys(): 2374 customStat["payIn"][item["payment"]["currency"]] += payment 2375 2376 else: 2377 customStat["payIn"][item["payment"]["currency"]] = payment 2378 2379 # count withdrawals operations: 2380 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2381 if item["payment"]["currency"] in customStat["payOut"].keys(): 2382 customStat["payOut"][item["payment"]["currency"]] += payment 2383 2384 else: 2385 customStat["payOut"][item["payment"]["currency"]] = payment 2386 2387 # count dividends income: 2388 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2389 if item["payment"]["currency"] in customStat["divs"].keys(): 2390 customStat["divs"][item["payment"]["currency"]] += payment 2391 2392 else: 2393 customStat["divs"][item["payment"]["currency"]] = payment 2394 2395 # count coupon's income: 2396 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2397 if item["payment"]["currency"] in customStat["coupons"].keys(): 2398 customStat["coupons"][item["payment"]["currency"]] += payment 2399 2400 else: 2401 customStat["coupons"][item["payment"]["currency"]] = payment 2402 2403 # count broker commissions: 2404 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2405 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2406 customStat["brokerCom"][item["payment"]["currency"]] += payment 2407 2408 else: 2409 customStat["brokerCom"][item["payment"]["currency"]] = payment 2410 2411 # count service commissions: 2412 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2413 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2414 customStat["serviceCom"][item["payment"]["currency"]] += payment 2415 2416 else: 2417 customStat["serviceCom"][item["payment"]["currency"]] = payment 2418 2419 # count margin commissions: 2420 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2421 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2422 customStat["marginCom"][item["payment"]["currency"]] += payment 2423 2424 else: 2425 customStat["marginCom"][item["payment"]["currency"]] = payment 2426 2427 # count withholding taxes: 2428 elif "_TAX" in item["operationType"]: 2429 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2430 customStat["allTaxes"][item["payment"]["currency"]] += payment 2431 2432 else: 2433 customStat["allTaxes"][item["payment"]["currency"]] = payment 2434 2435 else: 2436 continue 2437 2438 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2439 2440 # --- view "Actions" lines: 2441 info.extend([ 2442 "| Report sections | | | | |\n", 2443 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2444 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2445 "| | Buy: {:<22} | {:<28} | | |\n".format( 2446 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2447 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2448 ), 2449 "| | Sell: {:<21} | {:<28} | | |\n".format( 2450 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2451 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2452 ), 2453 ]) 2454 2455 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2456 for key in opsKeys: 2457 if key == "rub": 2458 continue 2459 2460 info.extend([ 2461 "| | | {:<28} | | |\n".format( 2462 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2463 ), 2464 "| | | {:<28} | | |\n".format( 2465 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2466 ), 2467 ]) 2468 2469 info.append(splitLine1) 2470 2471 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2472 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2473 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2474 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2475 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2476 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2477 ) 2478 2479 # --- view "Payments" lines: 2480 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2481 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2482 2483 for key in paymentsKeys: 2484 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2485 2486 info.append(splitLine1) 2487 2488 # --- view "Commissions and taxes" lines: 2489 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2490 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2491 2492 for key in comKeys: 2493 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2494 2495 info.append(splitLine1) 2496 2497 info.extend([ 2498 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2499 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2500 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2501 ]) 2502 2503 else: 2504 info.append("Broker returned no operations during this period\n") 2505 2506 # --- view "Operations" section: 2507 for item in ops: 2508 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2509 continue 2510 2511 else: 2512 self.figi = item["figi"] if item["figi"] else "" 2513 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2514 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2515 2516 # group of deals during one day: 2517 if nextDay and item["date"].split("T")[0] != nextDay: 2518 info.append(splitLine2) 2519 nextDay = "" 2520 2521 else: 2522 nextDay = item["date"].split("T")[0] # saving current day for splitting 2523 2524 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2525 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2526 self.figi if self.figi else "—", 2527 instrument["ticker"] if instrument else "—", 2528 instrument["type"] if instrument else "—", 2529 item["quantity"] if int(item["quantity"]) > 0 else "—", 2530 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2531 TKS_OPERATION_STATES[item["state"]], 2532 TKS_OPERATION_TYPES[item["operationType"]], 2533 )) 2534 2535 infoText = "".join(info) 2536 2537 if show: 2538 if self.moreDebug: 2539 uLogger.debug("Records about history of a client's operations successfully received") 2540 2541 uLogger.info(infoText) 2542 2543 if self.reportFile: 2544 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2545 fH.write(infoText) 2546 2547 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2548 2549 return ops, customStat
Returns history operations between two given dates for current accountId.
If reportFile string is not empty then also save human-readable report.
Shows some statistical data of closed positions.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - show: if
Truethen also prints all records to the console. - showCancelled: if
Falsethen remove information about cancelled operations from the deals report.
Returns
original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2551 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2552 """ 2553 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2554 2555 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2556 Warning! Broker server used ISO UTC time by default. 2557 2558 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2559 Also, `historyFile` used to update history with `onlyMissing` parameter. 2560 2561 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2562 2563 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2564 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2565 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2566 `"hour"`, `"day"`. Default: `"hour"`. 2567 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2568 False by default. Warning! History appends only from last candle to current time 2569 with always update last candle! 2570 :param csvSep: separator if csv-file is used, `,` by default. 2571 :param show: if `True` then also prints Pandas DataFrame to the console. 2572 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2573 `["date", "time", "open", "high", "low", "close", "volume"]`. 2574 """ 2575 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2576 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2577 history = None # empty pandas object for history 2578 2579 if interval not in TKS_CANDLE_INTERVALS.keys(): 2580 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2581 raise Exception("Incorrect value") 2582 2583 if not (self.ticker or self.figi): 2584 uLogger.error("Ticker or FIGI must be defined!") 2585 raise Exception("Ticker or FIGI required") 2586 2587 if self.ticker and not self.figi: 2588 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2589 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2590 2591 if self.figi and not self.ticker: 2592 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2593 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2594 2595 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2596 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2597 if interval.lower() != "day": 2598 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2599 2600 delta = dtEnd - dtStart # current UTC time minus last time in file 2601 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2602 2603 # calculate history length in candles: 2604 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2605 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2606 length += 1 # to avoid fraction time 2607 2608 # calculate data blocks count: 2609 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2610 2611 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2612 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2613 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2614 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2615 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2616 2617 tempOld = None # pandas object for old history, if --only-missing key present 2618 lastTime = None # datetime object of last old candle in file 2619 2620 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2621 uLogger.debug("--only-missing key present, add only last missing candles...") 2622 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2623 2624 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2625 2626 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2627 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2628 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2629 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2630 2631 # get last datetime object from last string in file or minus 1 delta if file is empty: 2632 if len(tempOld) > 0: 2633 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2634 2635 else: 2636 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2637 2638 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2639 2640 responseJSONs = [] # raw history blocks of data 2641 2642 blockEnd = dtEnd 2643 for item in range(blocks): 2644 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2645 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2646 2647 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2648 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2649 )) 2650 2651 if blockStart == blockEnd: 2652 uLogger.debug("Skipped this zero-length block...") 2653 2654 else: 2655 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2656 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2657 self.body = str({ 2658 "figi": self.figi, 2659 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2660 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2661 "interval": TKS_CANDLE_INTERVALS[interval][0] 2662 }) 2663 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2664 2665 if "code" in responseJSON.keys(): 2666 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2667 2668 else: 2669 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2670 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2671 2672 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2673 2674 blockEnd = blockStart 2675 2676 printCount = len(responseJSONs) # candles to show in console 2677 if responseJSONs: 2678 tempHistory = pd.DataFrame( 2679 data={ 2680 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2681 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2682 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2683 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2684 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2685 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2686 "volume": [int(item["volume"]) for item in responseJSONs], 2687 }, 2688 index=range(len(responseJSONs)), 2689 columns=["date", "time", "open", "high", "low", "close", "volume"], 2690 ) 2691 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2692 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2693 2694 # append only newest candles to old history if --only-missing key present: 2695 if onlyMissing and tempOld is not None and lastTime is not None: 2696 index = 0 # find start index in tempHistory data: 2697 2698 for i, item in tempHistory.iterrows(): 2699 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2700 2701 if curTime == lastTime: 2702 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2703 index = i 2704 printCount = index + 1 2705 break 2706 2707 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2708 2709 else: 2710 history = tempHistory # if no `--only-missing` key then load full data from server 2711 2712 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2713 2714 if history is not None and not history.empty: 2715 if show: 2716 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2717 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2718 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2719 )) 2720 2721 else: 2722 uLogger.warning("Received an empty candles history!") 2723 2724 if self.historyFile is not None: 2725 if history is not None and not history.empty: 2726 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2727 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2728 2729 else: 2730 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2731 2732 else: 2733 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2734 2735 return history
This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).
History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01.
Warning! Broker server used ISO UTC time by default.
If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame.
Also, historyFile used to update history with onlyMissing parameter.
See also: LoadHistory() and ShowHistoryChart() methods.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - interval: this is a candle interval. Current available values are
"1min","5min","15min","hour","day". Default:"hour". - onlyMissing: if
Truethen add only last missing candles, do not request all history length fromstart. False by default. Warning! History appends only from last candle to current time with always update last candle! - csvSep: separator if csv-file is used,
,by default. - show: if
Truethen also prints Pandas DataFrame to the console.
Returns
Pandas DataFrame with prices history. Headers of columns are defined by default:
["date", "time", "open", "high", "low", "close", "volume"].
2737 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2738 """ 2739 Load candles history from csv-file and return Pandas DataFrame object. 2740 2741 See also: `History()` and `ShowHistoryChart()` methods. 2742 2743 :param filePath: path to csv-file to open. 2744 """ 2745 loadedHistory = None # init candles data object 2746 2747 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2748 2749 if os.path.exists(filePath): 2750 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2751 2752 tfStr = self.priceModel.FormattedDelta( 2753 self.priceModel.timeframe, 2754 "{days} days {hours}h {minutes}m {seconds}s", 2755 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2756 self.priceModel.timeframe, 2757 "{hours}h {minutes}m {seconds}s", 2758 ) 2759 2760 if loadedHistory is not None and not loadedHistory.empty: 2761 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2762 len(loadedHistory), 2763 tfStr, 2764 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2765 ) 2766 2767 else: 2768 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2769 2770 else: 2771 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2772 2773 return loadedHistory
Load candles history from csv-file and return Pandas DataFrame object.
See also: History() and ShowHistoryChart() methods.
Parameters
- filePath: path to csv-file to open.
2775 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2776 """ 2777 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2778 2779 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2780 Default: `index.html` (both for interact and non-interact candlesticks chart). 2781 2782 See also: `History()` and `LoadHistory()` methods. 2783 2784 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2785 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2786 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2787 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2788 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2789 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2790 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2791 """ 2792 if isinstance(candles, str): 2793 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2794 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2795 2796 elif isinstance(candles, pd.DataFrame): 2797 self.priceModel.prices = candles # set candles chain from variable 2798 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2799 2800 if "datetime" not in candles.columns: 2801 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2802 2803 else: 2804 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2805 raise Exception("Incorrect value") 2806 2807 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2808 2809 if interact: 2810 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2811 2812 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2813 2814 else: 2815 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2816 2817 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2818 2819 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart.
Default: index.html (both for interact and non-interact candlesticks chart).
See also: History() and LoadHistory() methods.
Parameters
- candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
- interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters If False then chain of candlesticks will render as not interactive Google Candlestick chart. See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
- openInBrowser: if True then immediately open chart in default browser, otherwise only path to
html-file prints to console. False by default, to avoid issues with
permissions deniedto html-file.
2821 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2822 """ 2823 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2824 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2825 2826 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2827 2828 :param operation: string "Buy" or "Sell". 2829 :param lots: volume, integer count of lots >= 1. 2830 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2831 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2832 :param expDate: string "Undefined" by default or local date in future, 2833 it is a string with format `%Y-%m-%d %H:%M:%S`. 2834 :return: JSON with response from broker server. 2835 """ 2836 if self.accountId is None or not self.accountId: 2837 uLogger.error("Variable `accountId` must be defined for using this method!") 2838 raise Exception("Account ID required") 2839 2840 if operation is None or not operation or operation not in ("Buy", "Sell"): 2841 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2842 raise Exception("Incorrect value") 2843 2844 if lots is None or lots < 1: 2845 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2846 lots = 1 2847 2848 if tp is None or tp < 0: 2849 tp = 0 2850 2851 if sl is None or sl < 0: 2852 sl = 0 2853 2854 if expDate is None or not expDate: 2855 expDate = "Undefined" 2856 2857 if not (self.ticker or self.figi): 2858 uLogger.error("Ticker or FIGI must be defined!") 2859 raise Exception("Ticker or FIGI required") 2860 2861 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 2862 self.ticker = instrument["ticker"] 2863 self.figi = instrument["figi"] 2864 2865 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2866 2867 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2868 self.body = str({ 2869 "figi": self.figi, 2870 "quantity": str(lots), 2871 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2872 "accountId": str(self.accountId), 2873 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2874 }) 2875 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2876 2877 if "orderId" in response.keys(): 2878 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2879 operation, response["orderId"], 2880 self.ticker, self.figi, lots, 2881 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2882 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2883 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2884 )) 2885 2886 if tp > 0: 2887 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2888 2889 if sl > 0: 2890 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2891 2892 else: 2893 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.") 2894 2895 return response
Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().
Parameters
- operation: string "Buy" or "Sell".
- lots: volume, integer count of lots >= 1.
- tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter
targetPriceinself.Order(). - sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter
targetPriceinself.Order(). - expDate: string "Undefined" by default or local date in future,
it is a string with format
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2897 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2898 """ 2899 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2900 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2901 2902 See also: `Order()` and `Trade()` docstrings. 2903 2904 :param lots: volume, integer count of lots >= 1. 2905 :param tp: float > 0, take profit price of stop-order. 2906 :param sl: float > 0, stop loss price of stop-order. 2907 :param expDate: it's a local date in future. 2908 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2909 :return: JSON with response from broker server. 2910 """ 2911 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2913 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2914 """ 2915 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2916 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2917 2918 See also: `Order()` and `Trade()` docstrings. 2919 2920 :param lots: volume, integer count of lots >= 1. 2921 :param tp: float > 0, take profit price of stop-order. 2922 :param sl: float > 0, stop loss price of stop-order. 2923 :param expDate: it's a local date in the future. 2924 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2925 :return: JSON with response from broker server. 2926 """ 2927 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in the future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2929 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 2930 """ 2931 Close position of given instruments. 2932 2933 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 2934 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 2935 This avoids unnecessary downloading data from the server. 2936 """ 2937 if instruments is None or not instruments: 2938 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 2939 raise Exception("Ticker or FIGI required") 2940 2941 if isinstance(instruments, str): 2942 instruments = [instruments] 2943 2944 uniqueInstruments = self.GetUniqueFIGIs(instruments) 2945 if uniqueInstruments: 2946 if portfolio is None or not portfolio: 2947 portfolio = self.Overview(show=False) 2948 2949 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 2950 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 2951 2952 for self.figi in uniqueInstruments: 2953 if self.figi not in allOpened: 2954 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi)) 2955 continue 2956 2957 # search open trade info about instrument by ticker: 2958 instrument = {} 2959 for iType in TKS_INSTRUMENTS: 2960 if instrument: 2961 break 2962 2963 for item in portfolio["stat"][iType]: 2964 if item["figi"] == self.figi: 2965 instrument = item 2966 break 2967 2968 if instrument: 2969 self.ticker = instrument["ticker"] 2970 self.figi = instrument["figi"] 2971 2972 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 2973 self.ticker, 2974 self.figi, 2975 int(instrument["volume"]), 2976 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 2977 )) 2978 2979 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 2980 2981 if tradeLots > 0: 2982 if instrument["blocked"] > 0: 2983 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 2984 instrument["blocked"], 2985 self.ticker, 2986 tradeLots, 2987 )) 2988 2989 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 2990 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 2991 2992 else: 2993 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
Close position of given instruments.
Parameters
- instruments: list of instruments defined by tickers or FIGIs that must be closed.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
2995 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 2996 """ 2997 Close all positions of given instruments with defined type. 2998 2999 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3000 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3001 This avoids unnecessary downloading data from the server. 3002 """ 3003 if iType not in TKS_INSTRUMENTS: 3004 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3005 3006 else: 3007 if portfolio is None or not portfolio: 3008 portfolio = self.Overview(show=False) 3009 3010 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3011 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3012 3013 if tickers and portfolio: 3014 self.CloseTrades(tickers, portfolio) 3015 3016 else: 3017 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
Close all positions of given instruments with defined type.
Parameters
- iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3019 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3020 """ 3021 Universal method to create market or limit orders with all available parameters for current `accountId`. 3022 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3023 3024 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3025 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3026 3027 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3028 then broker immediately open market order as you can do simple --buy or --sell operations! 3029 3030 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3031 When current price will go up or down to target price value then broker opens a limit order. 3032 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3033 3034 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3035 3036 :param operation: string "Buy" or "Sell". 3037 :param orderType: string "Limit" or "Stop". 3038 :param lots: volume, integer count of lots >= 1. 3039 :param targetPrice: target price > 0. This is open trade price for limit order. 3040 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3041 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3042 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3043 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3044 Stop loss order always executed by market price. 3045 :param expDate: string "Undefined" by default or local date in future. 3046 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3047 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3048 A limit order has no expiration date, it lasts until the end of the trading day. 3049 :return: JSON with response from broker server. 3050 """ 3051 if self.accountId is None or not self.accountId: 3052 uLogger.error("Variable `accountId` must be defined for using this method!") 3053 raise Exception("Account ID required") 3054 3055 if operation is None or not operation or operation not in ("Buy", "Sell"): 3056 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3057 raise Exception("Incorrect value") 3058 3059 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3060 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3061 raise Exception("Incorrect value") 3062 3063 if lots is None or lots < 1: 3064 uLogger.error("You must define trade volume > 0: integer count of lots!") 3065 raise Exception("Incorrect value") 3066 3067 if targetPrice is None or targetPrice <= 0: 3068 uLogger.error("Target price for limit-order must be greater than 0!") 3069 raise Exception("Incorrect value") 3070 3071 if limitPrice is None or limitPrice <= 0: 3072 limitPrice = targetPrice 3073 3074 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3075 stopType = "Limit" 3076 3077 if expDate is None or not expDate: 3078 expDate = "Undefined" 3079 3080 if not (self.ticker or self.figi): 3081 uLogger.error("Tocker or FIGI must be defined!") 3082 raise Exception("Ticker or FIGI required") 3083 3084 response = {} 3085 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 3086 self.ticker = instrument["ticker"] 3087 self.figi = instrument["figi"] 3088 3089 if orderType == "Limit": 3090 uLogger.debug( 3091 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3092 self.ticker, self.figi, 3093 operation, lots, targetPrice, instrument["currency"], 3094 )) 3095 3096 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3097 self.body = str({ 3098 "figi": self.figi, 3099 "quantity": str(lots), 3100 "price": FloatToNano(targetPrice), 3101 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3102 "accountId": str(self.accountId), 3103 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3104 }) 3105 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3106 3107 if "orderId" in response.keys(): 3108 uLogger.info( 3109 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3110 response["orderId"], 3111 self.ticker, self.figi, 3112 operation, lots, targetPrice, instrument["currency"], 3113 )) 3114 3115 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3116 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3117 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3118 targetPrice, instrument["currency"], 3119 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3120 )) 3121 3122 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3123 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3124 targetPrice, instrument["currency"], 3125 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3126 )) 3127 3128 else: 3129 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3130 3131 if orderType == "Stop": 3132 uLogger.debug( 3133 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3134 self.ticker, self.figi, 3135 operation, lots, 3136 targetPrice, instrument["currency"], 3137 limitPrice, instrument["currency"], 3138 stopType, expDate, 3139 )) 3140 3141 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3142 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3143 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3144 3145 body = { 3146 "figi": self.figi, 3147 "quantity": str(lots), 3148 "price": FloatToNano(limitPrice), 3149 "stopPrice": FloatToNano(targetPrice), 3150 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3151 "accountId": str(self.accountId), 3152 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3153 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3154 } 3155 3156 if expDateUTC: 3157 body["expireDate"] = expDateUTC 3158 3159 self.body = str(body) 3160 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3161 3162 if "stopOrderId" in response.keys(): 3163 uLogger.info( 3164 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3165 response["stopOrderId"], 3166 self.ticker, self.figi, 3167 operation, lots, 3168 targetPrice, instrument["currency"], 3169 limitPrice, instrument["currency"], 3170 TKS_STOP_ORDER_TYPES[stopOrderType], 3171 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3172 )) 3173 3174 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3175 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3176 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3177 targetPrice, instrument["currency"], 3178 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3179 )) 3180 3181 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3182 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3183 targetPrice, instrument["currency"], 3184 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3185 )) 3186 3187 else: 3188 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3189 3190 return response
Universal method to create market or limit orders with all available parameters for current accountId.
See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().
If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!
If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
Only one attempt and no retry for opens order. If network issue occurred you can create new request.
Parameters
- operation: string "Buy" or "Sell".
- orderType: string "Limit" or "Stop".
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
- limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
- stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns
JSON with response from broker server.
3192 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3193 """ 3194 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3195 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3196 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3197 See also: `Order()` docstring. 3198 3199 :param lots: volume, integer count of lots >= 1. 3200 :param targetPrice: target price > 0. This is open trade price for limit order. 3201 :return: JSON with response from broker server. 3202 """ 3203 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Buy limit-order (below current price). You must specify only 2 parameters:
lots and target price to open buy limit-order. If you try to create buy limit-order above current price then
broker immediately open Buy market order, such as if you do simple --buy operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3205 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3206 """ 3207 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3208 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3209 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3210 target price value then broker opens a limit order. See also: `Order()` docstring. 3211 3212 :param lots: volume, integer count of lots >= 1. 3213 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3214 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3215 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3216 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3217 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3218 :param expDate: string "Undefined" by default or local date in future. 3219 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3220 This date is converting to UTC format for server. 3221 :return: JSON with response from broker server. 3222 """ 3223 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order.
In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for buy stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3225 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3226 """ 3227 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3228 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3229 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3230 See also: `Order()` docstring. 3231 3232 :param lots: volume, integer count of lots >= 1. 3233 :param targetPrice: target price > 0. This is open trade price for limit order. 3234 :return: JSON with response from broker server. 3235 """ 3236 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Sell limit-order (above current price). You must specify only 2 parameters:
lots and target price to open sell limit-order. If you try to create sell limit-order below current price then
broker immediately open Sell market order, such as if you do simple --sell operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3238 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3239 """ 3240 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3241 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3242 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3243 target price value then broker opens a limit order. See also: `Order()` docstring. 3244 3245 :param lots: volume, integer count of lots >= 1. 3246 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3247 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3248 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3249 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3250 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3251 :param expDate: string "Undefined" by default or local date in future. 3252 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3253 This date is converting to UTC format for server. 3254 :return: JSON with response from broker server. 3255 """ 3256 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order.
In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for sell stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3258 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3259 """ 3260 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3261 3262 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3263 :param allOrdersIDs: pre-received lists of all active pending orders. 3264 This avoids unnecessary downloading data from the server. 3265 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3266 """ 3267 if self.accountId is None or not self.accountId: 3268 uLogger.error("Variable `accountId` must be defined for using this method!") 3269 raise Exception("Account ID required") 3270 3271 if orderIDs: 3272 if allOrdersIDs is None or not allOrdersIDs: 3273 rawOrders = self.RequestPendingOrders() 3274 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3275 3276 if allStopOrdersIDs is None or not allStopOrdersIDs: 3277 rawStopOrders = self.RequestStopOrders() 3278 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3279 3280 for orderID in orderIDs: 3281 idInPendingOrders = orderID in allOrdersIDs 3282 idInStopOrders = orderID in allStopOrdersIDs 3283 3284 if not (idInPendingOrders or idInStopOrders): 3285 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3286 continue 3287 3288 else: 3289 if idInPendingOrders: 3290 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3291 3292 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3293 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3294 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3295 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3296 3297 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3298 if self.moreDebug: 3299 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3300 3301 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3302 3303 else: 3304 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3305 3306 elif idInStopOrders: 3307 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3308 3309 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3310 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3311 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3312 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3313 3314 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3315 if self.moreDebug: 3316 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3317 3318 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3319 3320 else: 3321 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3322 3323 else: 3324 continue
Cancel order or list of orders by its orderId or stopOrderId for current accountId.
Parameters
- orderIDs: list of integers with
orderIdorstopOrderId. - allOrdersIDs: pre-received lists of all active pending orders. This avoids unnecessary downloading data from the server.
- allStopOrdersIDs: pre-received lists of all active stop orders.
3326 def CloseAllOrders(self) -> None: 3327 """ 3328 Gets a list of open pending and stop orders and cancel it all. 3329 """ 3330 rawOrders = self.RequestPendingOrders() 3331 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3332 lenOrders = len(allOrdersIDs) 3333 3334 rawStopOrders = self.RequestStopOrders() 3335 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3336 lenSOrders = len(allStopOrdersIDs) 3337 3338 if lenOrders > 0 or lenSOrders > 0: 3339 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3340 3341 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3342 3343 else: 3344 uLogger.info("Orders not found, nothing to cancel.")
Gets a list of open pending and stop orders and cancel it all.
3346 def CloseAll(self, *args) -> None: 3347 """ 3348 Close all available (not blocked) opened trades and orders. 3349 3350 Also, you can select one or more keywords case-insensitive: 3351 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3352 3353 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3354 """ 3355 overview = self.Overview(show=False) # get all open trades info 3356 3357 if len(args) == 0: 3358 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3359 self.CloseAllOrders() # close all pending and stop orders 3360 3361 for iType in TKS_INSTRUMENTS: 3362 if iType != "Currencies": 3363 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3364 3365 else: 3366 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3367 lowerArgs = [x.lower() for x in args] 3368 3369 if "orders" in lowerArgs: 3370 self.CloseAllOrders() # close all pending and stop orders 3371 3372 for iType in TKS_INSTRUMENTS: 3373 if iType.lower() in lowerArgs and iType != "Currencies": 3374 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
Close all available (not blocked) opened trades and orders.
Also, you can select one or more keywords case-insensitive:
orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.
Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.
3376 @staticmethod 3377 def ParseOrderParameters(operation, **inputParameters): 3378 """ 3379 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3380 3381 :param operation: string "Buy" or "Sell". 3382 :param inputParameters: this is dict of strings that looks like this 3383 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3384 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3385 "prices" key: one or more prices to open limit-orders 3386 Counts of values in lots and prices lists must be equals! 3387 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3388 """ 3389 # TODO: update order grid work with api v2 3390 pass 3391 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3392 # 3393 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3394 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3395 # raise Exception("Incorrect value") 3396 # 3397 # if "l" in inputParameters.keys(): 3398 # inputParameters["lots"] = inputParameters.pop("l") 3399 # 3400 # if "p" in inputParameters.keys(): 3401 # inputParameters["prices"] = inputParameters.pop("p") 3402 # 3403 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3404 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3405 # raise Exception("Incorrect value") 3406 # 3407 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3408 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3409 # 3410 # if len(lots) != len(prices): 3411 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3412 # raise Exception("Incorrect value") 3413 # 3414 # uLogger.debug("Extracted parameters for orders:") 3415 # uLogger.debug("lots = {}".format(lots)) 3416 # uLogger.debug("prices = {}".format(prices)) 3417 # 3418 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3419 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3420 # uLogger.debug("Order parameters: {}".format(result)) 3421 # 3422 # return result
Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
Parameters
- operation: string "Buy" or "Sell".
- inputParameters: this is dict of strings that looks like this
{"lots": "L_int,...", "prices": "P_float,..."}where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns
list of dictionaries with all lots and prices to open orders that looks like this
[{"lot": lots_1, "price": price_1}, {...}, ...]
3424 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3425 """ 3426 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3427 3428 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3429 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3430 """ 3431 result = False 3432 msg = "Instrument not defined!" 3433 3434 if portfolio is None or not portfolio: 3435 portfolio = self.Overview(show=False) 3436 3437 if self.ticker: 3438 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3439 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3440 3441 for iType in TKS_INSTRUMENTS: 3442 for instrument in portfolio["stat"][iType]: 3443 if instrument["ticker"] == self.ticker: 3444 result = True 3445 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3446 break 3447 3448 elif self.figi: 3449 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3450 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3451 3452 for iType in TKS_INSTRUMENTS: 3453 for instrument in portfolio["stat"][iType]: 3454 if instrument["figi"] == self.figi: 3455 result = True 3456 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3457 break 3458 3459 else: 3460 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3461 3462 uLogger.debug(msg) 3463 3464 return result
Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif portfolio contains open position with given instrument,Falseotherwise.
3466 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3467 """ 3468 Returns instrument from the user's portfolio if it presents there. 3469 Instrument must be defined by `ticker` (highly priority) or `figi`. 3470 3471 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3472 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3473 """ 3474 result = None 3475 msg = "Instrument not defined!" 3476 3477 if portfolio is None or not portfolio: 3478 portfolio = self.Overview(show=False) 3479 3480 if self.ticker: 3481 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker)) 3482 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3483 3484 for iType in TKS_INSTRUMENTS: 3485 for instrument in portfolio["stat"][iType]: 3486 if instrument["ticker"] == self.ticker: 3487 result = instrument 3488 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3489 break 3490 3491 elif self.figi: 3492 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3493 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3494 3495 for iType in TKS_INSTRUMENTS: 3496 for instrument in portfolio["stat"][iType]: 3497 if instrument["figi"] == self.figi: 3498 result = instrument 3499 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3500 break 3501 3502 else: 3503 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3504 3505 uLogger.debug(msg) 3506 3507 return result
Returns instrument from the user's portfolio if it presents there.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
dict with instrument if portfolio contains open position with this instrument,
Noneotherwise.
3509 def RequestLimits(self) -> dict: 3510 """ 3511 Method for obtaining the available funds for withdrawal for current `accountId`. 3512 3513 See also: 3514 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3515 - `OverviewLimits()` method 3516 3517 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3518 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3519 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3520 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3521 """ 3522 if self.accountId is None or not self.accountId: 3523 uLogger.error("Variable `accountId` must be defined for using this method!") 3524 raise Exception("Account ID required") 3525 3526 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3527 3528 self.body = str({"accountId": self.accountId}) 3529 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3530 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3531 3532 if self.moreDebug: 3533 uLogger.debug("Records about available funds for withdrawal successfully received") 3534 3535 return rawLimits
Method for obtaining the available funds for withdrawal for current accountId.
See also:
- REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
OverviewLimits()method
Returns
dict with raw data from server that contains free funds for withdrawal. Example of dict:
{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Heremoneyis an array of portfolio currency positions,blockedis an array of blocked currency positions of the portfolio andblockedGuaranteeis locked money under collateral for futures.
3537 def OverviewLimits(self, show: bool = False) -> dict: 3538 """ 3539 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3540 3541 See also: `RequestLimits()`. 3542 3543 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3544 :return: dict with raw parsed data from server and some calculated statistics about it. 3545 """ 3546 if self.accountId is None or not self.accountId: 3547 uLogger.error("Variable `accountId` must be defined for using this method!") 3548 raise Exception("Account ID required") 3549 3550 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3551 3552 view = { 3553 "rawLimits": rawLimits, 3554 "limits": { # parsed data for every currency: 3555 "money": { # this is an array of portfolio currency positions 3556 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3557 }, 3558 "blocked": { # this is an array of blocked currency 3559 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3560 }, 3561 "blockedGuarantee": { # this is locked money under collateral for futures 3562 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3563 }, 3564 }, 3565 } 3566 3567 # --- Prepare text table with limits in human-readable format: 3568 if show: 3569 info = [ 3570 "# Withdrawal limits\n\n", 3571 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3572 "* **Account ID:** [{}]\n".format(self.accountId), 3573 ] 3574 3575 if view["limits"]["money"]: 3576 info.extend([ 3577 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3578 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3579 ]) 3580 3581 else: 3582 info.append("\nNo withdrawal limits\n") 3583 3584 for curr in view["limits"]["money"].keys(): 3585 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3586 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3587 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3588 3589 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3590 "[{}]".format(curr), 3591 "{:.2f}".format(view["limits"]["money"][curr]), 3592 "{:.2f}".format(availableMoney), 3593 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3594 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3595 ) 3596 3597 if curr == "rub": 3598 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3599 3600 else: 3601 info.append(infoStr) 3602 3603 infoText = "".join(info) 3604 3605 uLogger.info(infoText) 3606 3607 if self.withdrawalLimitsFile: 3608 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3609 fH.write(infoText) 3610 3611 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3612 3613 return view
Method for parsing and show table with available funds for withdrawal for current accountId.
See also: RequestLimits().
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print withdrawal limits to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
3615 def RequestAccounts(self) -> dict: 3616 """ 3617 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3618 3619 See also: 3620 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3621 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3622 - `OverviewUserInfo()` method 3623 3624 :return: dict with raw data from server that contains accounts info. Example of dict: 3625 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3626 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3627 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3628 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3629 """ 3630 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3631 3632 self.body = str({}) 3633 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3634 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3635 3636 if self.moreDebug: 3637 uLogger.debug("Records about available accounts successfully received") 3638 3639 return rawAccounts
Method for requesting all brokerage accounts (accountIds) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
- What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
OverviewUserInfo()method
Returns
dict with raw data from server that contains accounts info. Example of dict:
{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. IfclosedDate="1970-01-01T00:00:00Z"it means that account is active now.
3641 def RequestUserInfo(self) -> dict: 3642 """ 3643 Method for requesting common user's information. 3644 3645 See also: 3646 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3647 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3648 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3649 - `OverviewUserInfo()` method 3650 3651 :return: dict with raw data from server that contains user's information. Example of dict: 3652 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3653 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3654 """ 3655 uLogger.debug("Requesting common user's information. Wait, please...") 3656 3657 self.body = str({}) 3658 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3659 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3660 3661 if self.moreDebug: 3662 uLogger.debug("Records about current user successfully received") 3663 3664 return rawUserInfo
Method for requesting common user's information.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
- What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
- What does
qualified_for_work_withfield mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with OverviewUserInfo()method
Returns
dict with raw data from server that contains user's information. Example of dict:
{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.
3666 def RequestMarginStatus(self, accountId: str = None) -> dict: 3667 """ 3668 Method for requesting margin calculation for defined account ID. 3669 3670 See also: 3671 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3672 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3673 - `OverviewUserInfo()` method 3674 3675 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3676 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3677 Example of responses: 3678 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3679 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3680 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3681 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3682 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3683 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3684 """ 3685 if accountId is None or not accountId: 3686 if self.accountId is None or not self.accountId: 3687 uLogger.error("Variable `accountId` must be defined for using this method!") 3688 raise Exception("Account ID required") 3689 3690 else: 3691 accountId = self.accountId # use `self.accountId` (main ID) by default 3692 3693 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3694 3695 self.body = str({"accountId": accountId}) 3696 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3697 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3698 3699 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3700 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3701 rawMargin = {} 3702 3703 else: 3704 if self.moreDebug: 3705 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3706 3707 return rawMargin
Method for requesting margin calculation for defined account ID.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
- What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
OverviewUserInfo()method
Parameters
- accountId: string with numeric account ID. If
None, then used class fieldaccountId.
Returns
dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400:
{"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns:{}. status code 200:{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.
3709 def RequestTariffLimits(self) -> dict: 3710 """ 3711 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3712 3713 See also: 3714 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3715 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3716 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3717 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3718 - `OverviewUserInfo()` method 3719 3720 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3721 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3722 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3723 """ 3724 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3725 3726 self.body = str({}) 3727 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3728 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3729 3730 if self.moreDebug: 3731 uLogger.debug("Records with limits of current tariff successfully received") 3732 3733 return rawTariffLimits
Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
- What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
- Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
- Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
OverviewUserInfo()method
Returns
dict with raw data from server that contains limits of current tariff. Example of dict:
{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.
3735 def RequestBondCoupons(self, iJSON: dict) -> dict: 3736 """ 3737 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3738 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3739 All dates are in UTC timezone. 3740 3741 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3742 Documentation: 3743 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3744 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3745 3746 See also: `ExtendBondsData()`. 3747 3748 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3749 If raw iJSON is not data of bond then server returns an error [400] with message: 3750 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3751 :return: dictionary with bond payment calendar. Response example 3752 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3753 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3754 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3755 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3756 """ 3757 if iJSON["figi"] is None or not iJSON["figi"]: 3758 uLogger.error("FIGI must be defined for using this method!") 3759 raise Exception("FIGI required") 3760 3761 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3762 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3763 3764 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3765 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3766 self.figi, 3767 startDate, 3768 endDate, 3769 )) 3770 3771 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3772 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3773 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 3774 3775 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3776 uLogger.warning("Instrument type is not bond!") 3777 3778 else: 3779 if self.moreDebug: 3780 uLogger.debug("Records about bond payment calendar successfully received") 3781 3782 return calendar
Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z".
All dates are in UTC timezone.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:
- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
- response: https://tinkoff.github.io/investAPI/instruments/#coupon
See also: ExtendBondsData().
Parameters
- iJSON: raw json data of a bond from broker server, example
iJSON = self.iList["Bonds"][self.ticker]If raw iJSON is not data of bond then server returns an error [400] with message:{"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns
dictionary with bond payment calendar. Response example
{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}
3784 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3785 """ 3786 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3787 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3788 coupon yields, current yields and some statistics etc. 3789 3790 WARNING! This is too long operation if a lot of bonds requested from broker server. 3791 3792 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3793 3794 :param instruments: list of strings with tickers or FIGIs. 3795 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3796 for further used by data scientists or stock analytics. 3797 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3798 In XLSX-file and Pandas DataFrame fields mean: 3799 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3800 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3801 """ 3802 if instruments is None or not instruments: 3803 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3804 raise Exception("Ticker or FIGI required") 3805 3806 if isinstance(instruments, str): 3807 instruments = [instruments] 3808 3809 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3810 3811 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3812 3813 iCount = len(uniqueInstruments) 3814 tooLong = iCount >= 20 3815 if tooLong: 3816 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3817 3818 bonds = None 3819 for i, self.figi in enumerate(uniqueInstruments): 3820 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3821 3822 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3823 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3824 rawBond = self.SearchByFIGI(requestPrice=True) 3825 3826 # Widen raw data with UTC current time (iData["actualDateTime"]): 3827 actualDate = datetime.now(tzutc()) 3828 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3829 3830 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3831 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3832 3833 # Replace some values with human-readable: 3834 iData["nominalCurrency"] = iData["nominal"]["currency"] 3835 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3836 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3837 iData["aciCurrency"] = iData["aciValue"]["currency"] 3838 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3839 iData["issueSize"] = int(iData["issueSize"]) 3840 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3841 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3842 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3843 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3844 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3845 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3846 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3847 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3848 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3849 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3850 3851 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3852 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3853 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3854 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3855 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3856 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3857 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3858 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3859 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3860 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3861 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3862 3863 # Widen raw data with calendar data from `rawCalendar` values: 3864 calendarData = [] 3865 if "events" in iData["rawCalendar"].keys(): 3866 for item in iData["rawCalendar"]["events"]: 3867 calendarData.append({ 3868 "couponDate": item["couponDate"], 3869 "couponNumber": int(item["couponNumber"]), 3870 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3871 "payCurrency": item["payOneBond"]["currency"], 3872 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3873 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3874 "couponStartDate": item["couponStartDate"], 3875 "couponEndDate": item["couponEndDate"], 3876 "couponPeriod": item["couponPeriod"], 3877 }) 3878 3879 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3880 if "maturityDate" not in iData.keys(): 3881 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3882 3883 # Widen raw data with Coupon Rate. 3884 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3885 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3886 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3887 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3888 3889 # Widen raw data with Yield to Maturity (YTM) on current date. 3890 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3891 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3892 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3893 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3894 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3895 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3896 3897 iData["calendar"] = calendarData # adds calendar at the end 3898 3899 # Remove not used data: 3900 iData.pop("uid") 3901 iData.pop("positionUid") 3902 iData.pop("currentPrice") 3903 iData.pop("rawCalendar") 3904 3905 colNames = list(iData.keys()) 3906 if bonds is None: 3907 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3908 3909 else: 3910 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3911 3912 else: 3913 uLogger.warning("Instrument is not a bond!") 3914 3915 processed = round(100 * (i + 1) / iCount, 1) 3916 if tooLong and processed % 5 == 0: 3917 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3918 3919 else: 3920 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3921 3922 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3923 3924 # Saving bonds from Pandas DataFrame to XLSX sheet: 3925 if xlsx and self.bondsXLSXFile: 3926 with pd.ExcelWriter( 3927 path=self.bondsXLSXFile, 3928 date_format=TKS_DATE_FORMAT, 3929 datetime_format=TKS_DATE_TIME_FORMAT, 3930 mode="w", 3931 ) as writer: 3932 bonds.to_excel( 3933 writer, 3934 sheet_name="Extended bonds data", 3935 index=True, 3936 encoding="UTF-8", 3937 freeze_panes=(1, 1), 3938 ) # saving as XLSX-file with freeze first row and column as headers 3939 3940 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 3941 3942 return bonds
Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().
Parameters
- instruments: list of strings with tickers or FIGIs.
- xlsx: if True then also exports Pandas DataFrame to xlsx-file
bondsXLSXFile, defaultext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns
wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3944 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 3945 """ 3946 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 3947 3948 WARNING! This is too long operation if a lot of bonds requested from broker server. 3949 3950 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 3951 3952 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 3953 extended information about bonds: main info, current prices, bond payment calendar, 3954 coupon yields, current yields and some statistics etc. 3955 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 3956 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 3957 for further used by data scientists or stock analytics. 3958 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 3959 """ 3960 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 3961 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 3962 3963 uLogger.debug("Generating bond payments calendar data. Wait, please...") 3964 3965 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 3966 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 3967 calendar = None 3968 for bond in extBonds.iterrows(): 3969 for item in bond[1]["calendar"]: 3970 cData = { 3971 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 3972 "couponDate": item["couponDate"], 3973 "figi": bond[1]["figi"], 3974 "ticker": bond[1]["ticker"], 3975 "name": bond[1]["name"], 3976 "couponNumber": item["couponNumber"], 3977 "payOneBond": item["payOneBond"], 3978 "payCurrency": item["payCurrency"], 3979 "couponType": item["couponType"], 3980 "couponPeriod": item["couponPeriod"], 3981 "fixDate": item["fixDate"], 3982 "couponStartDate": item["couponStartDate"], 3983 "couponEndDate": item["couponEndDate"], 3984 } 3985 3986 if calendar is None: 3987 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 3988 3989 else: 3990 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 3991 3992 if calendar is not None: 3993 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 3994 3995 # Saving calendar from Pandas DataFrame to XLSX sheet: 3996 if xlsx: 3997 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 3998 3999 with pd.ExcelWriter( 4000 path=xlsxCalendarFile, 4001 date_format=TKS_DATE_FORMAT, 4002 datetime_format=TKS_DATE_TIME_FORMAT, 4003 mode="w", 4004 ) as writer: 4005 humanReadable = calendar.copy(deep=True) 4006 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4007 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4008 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4009 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4010 humanReadable.columns = colNames # human-readable column names 4011 4012 humanReadable.to_excel( 4013 writer, 4014 sheet_name="Bond payments calendar", 4015 index=False, 4016 encoding="UTF-8", 4017 freeze_panes=(1, 2), 4018 ) # saving as XLSX-file with freeze first row and column as headers 4019 4020 del humanReadable # release df in memory 4021 4022 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4023 4024 return calendar
Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowBondsCalendar(), ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - xlsx: if True then also exports Pandas DataFrame to file
calendarFile+".xlsx",calendar.xlsxby default, for further used by data scientists or stock analytics.
Returns
Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4026 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4027 """ 4028 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4029 Also, creates Markdown file with calendar data, `calendar.md` by default. 4030 4031 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4032 4033 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4034 extended information about bonds: main info, current prices, bond payment calendar, 4035 coupon yields, current yields and some statistics etc. 4036 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4037 :param show: if `True` then also printing bonds payment calendar to the console, 4038 otherwise save to file `calendarFile` only. `False` by default. 4039 :return: multilines text in Markdown format with bonds payment calendar as a table. 4040 """ 4041 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4042 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4043 4044 infoText = "# Bond payments calendar\n\n" 4045 4046 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4047 4048 if not (calendar is None or calendar.empty): 4049 splitLine = "| | | | | | | | | |\n" 4050 4051 info = [ 4052 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4053 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4054 ] 4055 4056 newMonth = False 4057 notOneBond = calendar["figi"].nunique() > 1 4058 for i, bond in enumerate(calendar.iterrows()): 4059 if newMonth and notOneBond: 4060 info.append(splitLine) 4061 4062 info.append( 4063 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4064 " √" if bond[1]["paid"] else " —", 4065 bond[1]["couponDate"].split("T")[0], 4066 bond[1]["figi"], 4067 bond[1]["ticker"], 4068 bond[1]["couponNumber"], 4069 "{} {}".format( 4070 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4071 bond[1]["payCurrency"], 4072 ), 4073 bond[1]["couponType"], 4074 bond[1]["couponPeriod"], 4075 bond[1]["fixDate"].split("T")[0], 4076 ) 4077 ) 4078 4079 if i < len(calendar.values) - 1: 4080 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4081 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4082 newMonth = False if curDate.month == nextDate.month else True 4083 4084 else: 4085 newMonth = False 4086 4087 infoText += "".join(info) 4088 4089 if show: 4090 uLogger.info("{}".format(infoText)) 4091 4092 if self.calendarFile is not None: 4093 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4094 fH.write(infoText) 4095 4096 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4097 4098 else: 4099 infoText += "No data\n" 4100 4101 return infoText
Show bond payments calendar as a table. One row in input bonds dataframe contains one bond.
Also, creates Markdown file with calendar data, calendar.md by default.
See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - show: if
Truethen also printing bonds payment calendar to the console, otherwise save to filecalendarFileonly.Falseby default.
Returns
multilines text in Markdown format with bonds payment calendar as a table.
4103 def OverviewAccounts(self, show: bool = False) -> dict: 4104 """ 4105 Method for parsing and show simple table with all available user accounts. 4106 4107 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4108 4109 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4110 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4111 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4112 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4113 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4114 "closed": "—", "access": "Full access" }, ...}}` 4115 """ 4116 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4117 4118 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4119 accounts = { 4120 item["id"]: { 4121 "type": TKS_ACCOUNT_TYPES[item["type"]], 4122 "name": item["name"], 4123 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4124 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4125 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4126 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4127 } for item in rawAccounts["accounts"] 4128 } 4129 4130 # Raw and parsed data with some fields replaced in "stat" section: 4131 view = { 4132 "rawAccounts": rawAccounts, 4133 "stat": accounts, 4134 } 4135 4136 # --- Prepare simple text table with only accounts data in human-readable format: 4137 if show: 4138 info = [ 4139 "# User accounts\n\n", 4140 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4141 "| Account ID | Type | Status | Name |\n", 4142 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4143 ] 4144 4145 for account in view["stat"].keys(): 4146 info.extend([ 4147 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4148 account, 4149 view["stat"][account]["type"], 4150 view["stat"][account]["status"], 4151 view["stat"][account]["name"], 4152 ) 4153 ]) 4154 4155 infoText = "".join(info) 4156 4157 uLogger.info(infoText) 4158 4159 if self.userAccountsFile: 4160 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4161 fH.write(infoText) 4162 4163 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4164 4165 return view
Method for parsing and show simple table with all available user accounts.
See also: RequestAccounts() and OverviewUserInfo() methods.
Parameters
- show: if
Falsethen only dictionary with accounts data returns, ifTruethen also print it to log.
Returns
dict with parsed accounts data received from
RequestAccounts()method. Example of dict:view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}
4167 def OverviewUserInfo(self, show: bool = False) -> dict: 4168 """ 4169 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4170 4171 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4172 4173 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4174 :return: dict with raw parsed data from server and some calculated statistics about it. 4175 """ 4176 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4177 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4178 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4179 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4180 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4181 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4182 4183 # This is dict with parsed common user data: 4184 userInfo = { 4185 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4186 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4187 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4188 "tariff": rawUserInfo["tariff"], 4189 } 4190 4191 # This is an array of dict with parsed margin statuses for every account IDs: 4192 margins = {} 4193 for accountId in accounts.keys(): 4194 if rawMargins[accountId]: 4195 margins[accountId] = { 4196 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4197 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4198 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4199 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4200 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4201 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4202 } 4203 4204 else: 4205 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4206 4207 unary = {} # unary-connection limits 4208 for item in rawTariffLimits["unaryLimits"]: 4209 if item["limitPerMinute"] in unary.keys(): 4210 unary[item["limitPerMinute"]].extend(item["methods"]) 4211 4212 else: 4213 unary[item["limitPerMinute"]] = item["methods"] 4214 4215 stream = {} # stream-connection limits 4216 for item in rawTariffLimits["streamLimits"]: 4217 if item["limit"] in stream.keys(): 4218 stream[item["limit"]].extend(item["streams"]) 4219 4220 else: 4221 stream[item["limit"]] = item["streams"] 4222 4223 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4224 limits = { 4225 "unary": unary, 4226 "stream": stream, 4227 } 4228 4229 # Raw and parsed data as an output result: 4230 view = { 4231 "rawUserInfo": rawUserInfo, 4232 "rawAccounts": rawAccounts, 4233 "rawMargins": rawMargins, 4234 "rawTariffLimits": rawTariffLimits, 4235 "stat": { 4236 "userInfo": userInfo, 4237 "accounts": accounts, 4238 "margins": margins, 4239 "limits": limits, 4240 }, 4241 } 4242 4243 # --- Prepare text table with user information in human-readable format: 4244 if show: 4245 info = [ 4246 "# Full user information\n\n", 4247 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4248 "## Common information\n\n", 4249 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4250 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4251 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4252 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4253 "\n## User accounts\n\n", 4254 ] 4255 4256 for account in view["stat"]["accounts"].keys(): 4257 info.extend([ 4258 "### ID: [{}]\n\n".format(account), 4259 "| Parameters | Values |\n", 4260 "|----------------------|--------------------------------------------------------------|\n", 4261 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4262 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4263 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4264 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4265 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4266 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4267 ]) 4268 4269 if margins[account]: 4270 info.extend([ 4271 "| Margin status: | Enabled |\n", 4272 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4273 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4274 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4275 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4276 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4277 ]) 4278 4279 else: 4280 info.append("| Margin status: | Disabled |\n\n") 4281 4282 info.extend([ 4283 "\n## Current user tariff limits\n", 4284 "\nSee also:\n", 4285 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4286 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4287 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4288 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4289 "\n### Unary limits\n", 4290 ]) 4291 4292 if unary: 4293 for key, values in sorted(unary.items()): 4294 info.append("\n* Max requests per minute: {}\n".format(key)) 4295 4296 for value in values: 4297 info.append(" - {}\n".format(value)) 4298 4299 else: 4300 info.append("\nNot available\n") 4301 4302 info.append("\n### Stream limits\n") 4303 4304 if stream: 4305 for key, values in sorted(stream.items()): 4306 info.append("\n* Max stream connections: {}\n".format(key)) 4307 4308 for value in values: 4309 info.append(" - {}\n".format(value)) 4310 4311 else: 4312 info.append("\nNot available\n") 4313 4314 infoText = "".join(info) 4315 4316 uLogger.info(infoText) 4317 4318 if self.userInfoFile: 4319 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4320 fH.write(infoText) 4321 4322 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4323 4324 return view
Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).
See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print user's data to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4327class Args: 4328 """ 4329 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4330 """ 4331 def __init__(self, **kwargs): 4332 self.__dict__.update(kwargs) 4333 4334 def __getattr__(self, item): 4335 return None
If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.
4338def ParseArgs(): 4339 """This function get and parse command line keys.""" 4340 parser = ArgumentParser() # command-line string parser 4341 4342 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4343 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4344 4345 # --- options: 4346 4347 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4348 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4349 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4350 4351 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4352 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4353 4354 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4355 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4356 4357 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4358 4359 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4360 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4361 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4362 4363 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4364 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4365 4366 # --- commands: 4367 4368 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4369 4370 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4371 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4372 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4373 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4374 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4375 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4376 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4377 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4378 4379 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4380 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4381 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4382 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4383 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4384 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4385 4386 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4387 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4388 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4389 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4390 4391 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4392 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4393 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4394 4395 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4396 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4397 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4398 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4399 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4400 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4401 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4402 4403 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4404 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4405 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4406 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4407 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4408 4409 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4410 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4411 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4412 4413 cmdArgs = parser.parse_args() 4414 return cmdArgs
This function get and parse command line keys.
4417def Main(**kwargs): 4418 """ 4419 Main function for work with TKSBrokerAPI in the console. 4420 4421 See examples: 4422 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4423 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4424 """ 4425 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4426 4427 if args.debug_level: 4428 uLogger.level = 10 # always debug level by default 4429 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4430 4431 exitCode = 0 4432 start = datetime.now(tzutc()) 4433 uLogger.debug("=-" * 50) 4434 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4435 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4436 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4437 )) 4438 4439 # trying to calculate full current version: 4440 buildVersion = __version__ 4441 try: 4442 v = version("tksbrokerapi") 4443 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4444 4445 except Exception: 4446 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4447 4448 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4449 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4450 4451 try: 4452 if args.version: 4453 print("TKSBrokerAPI {}".format(buildVersion)) 4454 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4455 4456 else: 4457 # Init class for trading with Tinkoff Broker: 4458 trader = TinkoffBrokerServer( 4459 token=args.token, 4460 accountId=args.account_id, 4461 useCache=not args.no_cache, 4462 ) 4463 4464 # --- set some options: 4465 4466 if args.more: 4467 trader.moreDebug = True 4468 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4469 4470 if args.ticker: 4471 ticker = args.ticker.upper() # Tickers may be upper case only 4472 4473 if ticker in trader.aliasesKeys: 4474 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4475 4476 else: 4477 trader.ticker = ticker 4478 4479 if args.figi: 4480 trader.figi = args.figi.upper() # FIGIs may be upper case only 4481 4482 if args.depth is not None: 4483 trader.depth = args.depth 4484 4485 # --- do one command: 4486 4487 if args.list: 4488 if args.output is not None: 4489 trader.instrumentsFile = args.output 4490 4491 trader.ShowInstrumentsInfo(show=True) 4492 4493 elif args.list_xlsx: 4494 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4495 4496 elif args.bonds_xlsx is not None: 4497 if args.output is not None: 4498 trader.bondsXLSXFile = args.output 4499 4500 if len(args.bonds_xlsx) == 0: 4501 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4502 4503 else: 4504 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4505 4506 elif args.search: 4507 if args.output is not None: 4508 trader.searchResultsFile = args.output 4509 4510 trader.SearchInstruments(pattern=args.search[0], show=True) 4511 4512 elif args.info: 4513 if not (args.ticker or args.figi): 4514 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4515 raise Exception("Ticker or FIGI required") 4516 4517 if args.output is not None: 4518 trader.infoFile = args.output 4519 4520 if args.ticker: 4521 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4522 4523 else: 4524 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4525 4526 elif args.calendar is not None: 4527 if args.output is not None: 4528 trader.calendarFile = args.output 4529 4530 if len(args.calendar) == 0: 4531 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4532 4533 else: 4534 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4535 4536 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4537 4538 elif args.price: 4539 if not (args.ticker or args.figi): 4540 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4541 raise Exception("Ticker or FIGI required") 4542 4543 trader.GetCurrentPrices(show=True) 4544 4545 elif args.prices is not None: 4546 if args.output is not None: 4547 trader.pricesFile = args.output 4548 4549 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4550 4551 elif args.overview: 4552 if args.output is not None: 4553 trader.overviewFile = args.output 4554 4555 trader.Overview(show=True, details="full") 4556 4557 elif args.overview_digest: 4558 if args.output is not None: 4559 trader.overviewDigestFile = args.output 4560 4561 trader.Overview(show=True, details="digest") 4562 4563 elif args.overview_positions: 4564 if args.output is not None: 4565 trader.overviewPositionsFile = args.output 4566 4567 trader.Overview(show=True, details="positions") 4568 4569 elif args.overview_orders: 4570 if args.output is not None: 4571 trader.overviewOrdersFile = args.output 4572 4573 trader.Overview(show=True, details="orders") 4574 4575 elif args.overview_analytics: 4576 if args.output is not None: 4577 trader.overviewAnalyticsFile = args.output 4578 4579 trader.Overview(show=True, details="analytics") 4580 4581 elif args.overview_calendar: 4582 if args.output is not None: 4583 trader.overviewAnalyticsFile = args.output 4584 4585 trader.Overview(show=True, details="calendar") 4586 4587 elif args.deals is not None: 4588 if args.output is not None: 4589 trader.reportFile = args.output 4590 4591 if 0 <= len(args.deals) < 3: 4592 trader.Deals( 4593 start=args.deals[0] if len(args.deals) >= 1 else None, 4594 end=args.deals[1] if len(args.deals) == 2 else None, 4595 show=True, # Always show deals report in console 4596 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4597 ) 4598 4599 else: 4600 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4601 raise Exception("Incorrect value") 4602 4603 elif args.history is not None: 4604 if args.output is not None: 4605 trader.historyFile = args.output 4606 4607 if 0 <= len(args.history) < 3: 4608 dataReceived = trader.History( 4609 start=args.history[0] if len(args.history) >= 1 else None, 4610 end=args.history[1] if len(args.history) == 2 else None, 4611 interval="hour" if args.interval is None or not args.interval else args.interval, 4612 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4613 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4614 show=True, # shows all downloaded candles in console 4615 ) 4616 4617 if args.render_chart is not None and dataReceived is not None: 4618 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4619 4620 trader.ShowHistoryChart( 4621 candles=dataReceived, 4622 interact=iChart, 4623 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4624 ) 4625 4626 else: 4627 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4628 raise Exception("Incorrect value") 4629 4630 elif args.load_history is not None: 4631 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4632 4633 if args.render_chart is not None and histData is not None: 4634 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4635 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4636 4637 trader.ShowHistoryChart( 4638 candles=histData, 4639 interact=iChart, 4640 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4641 ) 4642 4643 elif args.trade is not None: 4644 if 1 <= len(args.trade) <= 5: 4645 trader.Trade( 4646 operation=args.trade[0], 4647 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4648 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4649 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4650 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4651 ) 4652 4653 else: 4654 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4655 4656 elif args.buy is not None: 4657 if 0 <= len(args.buy) <= 4: 4658 trader.Buy( 4659 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4660 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4661 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4662 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4663 ) 4664 4665 else: 4666 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4667 4668 elif args.sell is not None: 4669 if 0 <= len(args.sell) <= 4: 4670 trader.Sell( 4671 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4672 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4673 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4674 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4675 ) 4676 4677 else: 4678 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4679 4680 elif args.order: 4681 if 4 <= len(args.order) <= 7: 4682 trader.Order( 4683 operation=args.order[0], 4684 orderType=args.order[1], 4685 lots=int(args.order[2]), 4686 targetPrice=float(args.order[3]), 4687 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4688 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4689 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4690 ) 4691 4692 else: 4693 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4694 4695 elif args.buy_limit: 4696 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4697 4698 elif args.sell_limit: 4699 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4700 4701 elif args.buy_stop: 4702 if 2 <= len(args.buy_stop) <= 7: 4703 trader.BuyStop( 4704 lots=int(args.buy_stop[0]), 4705 targetPrice=float(args.buy_stop[1]), 4706 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4707 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4708 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4709 ) 4710 4711 else: 4712 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4713 4714 elif args.sell_stop: 4715 if 2 <= len(args.sell_stop) <= 7: 4716 trader.SellStop( 4717 lots=int(args.sell_stop[0]), 4718 targetPrice=float(args.sell_stop[1]), 4719 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4720 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4721 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4722 ) 4723 4724 else: 4725 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4726 4727 # elif args.buy_order_grid is not None: 4728 # # update order grid work with api v2 4729 # if len(args.buy_order_grid) == 2: 4730 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4731 # 4732 # for order in orderParams: 4733 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4734 # 4735 # else: 4736 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4737 # 4738 # elif args.sell_order_grid is not None: 4739 # # update order grid work with api v2 4740 # if len(args.sell_order_grid) >= 2: 4741 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4742 # 4743 # for order in orderParams: 4744 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4745 # 4746 # else: 4747 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4748 4749 elif args.close_order is not None: 4750 trader.CloseOrders(args.close_order) # close only one order 4751 4752 elif args.close_orders is not None: 4753 trader.CloseOrders(args.close_orders) # close list of orders 4754 4755 elif args.close_trade: 4756 if not (args.ticker or args.figi): 4757 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4758 raise Exception("Ticker or FIGI required") 4759 4760 if args.ticker: 4761 trader.CloseTrades([args.ticker]) # close only one trade by ticker (priority) 4762 4763 else: 4764 trader.CloseTrades([args.figi]) # close only one trade by FIGI 4765 4766 elif args.close_trades is not None: 4767 trader.CloseTrades(args.close_trades) # close trades for list of tickers 4768 4769 elif args.close_all is not None: 4770 trader.CloseAll(*args.close_all) 4771 4772 elif args.limits: 4773 if args.output is not None: 4774 trader.withdrawalLimitsFile = args.output 4775 4776 trader.OverviewLimits(show=True) 4777 4778 elif args.user_info: 4779 if args.output is not None: 4780 trader.userInfoFile = args.output 4781 4782 trader.OverviewUserInfo(show=True) 4783 4784 elif args.account: 4785 if args.output is not None: 4786 trader.userAccountsFile = args.output 4787 4788 trader.OverviewAccounts(show=True) 4789 4790 else: 4791 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4792 raise Exception("There is no command to execute") 4793 4794 except Exception: 4795 trace = tb.format_exc() 4796 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4797 if e in trace: 4798 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4799 break 4800 4801 uLogger.debug(trace) 4802 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4803 exitCode = 255 # an error occurred, must be open a ticket for this issue 4804 4805 finally: 4806 finish = datetime.now(tzutc()) 4807 4808 if exitCode == 0: 4809 if args.more: 4810 uLogger.debug("All operations were finished success (summary code is 0).") 4811 4812 else: 4813 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4814 os.path.abspath(uLog.defaultLogFile), exitCode, 4815 )) 4816 4817 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4818 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4819 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4820 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4821 )) 4822 uLogger.debug("=-" * 50) 4823 4824 if not kwargs: 4825 sys.exit(exitCode) 4826 4827 else: 4828 return exitCode
Main function for work with TKSBrokerAPI in the console.
See examples: